iCode-TUI 0.29.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chrys/__init__.py +16 -0
- chrys/app/__init__.py +3 -0
- chrys/app/acp/__init__.py +3 -0
- chrys/app/acp/bridge.py +664 -0
- chrys/app/acp/content.py +132 -0
- chrys/app/acp/doc/frontend-api.md +425 -0
- chrys/app/acp/history.py +585 -0
- chrys/app/acp/server.py +1796 -0
- chrys/app/acp/session_manager.py +1815 -0
- chrys/app/acp/tool_status.py +45 -0
- chrys/app/cli/__init__.py +3 -0
- chrys/app/cli/acp.py +178 -0
- chrys/app/cli/app.py +165 -0
- chrys/app/cli/approvals.py +101 -0
- chrys/app/cli/headless.py +223 -0
- chrys/app/cli/launch_cwd.py +42 -0
- chrys/app/cli/profiles.py +298 -0
- chrys/app/cli/progress.py +857 -0
- chrys/app/cli/run.py +425 -0
- chrys/app/cli/serve.py +1593 -0
- chrys/app/cli/tool_summary.py +225 -0
- chrys/app/cli/trajectory.py +201 -0
- chrys/app/cli/workflow.py +601 -0
- chrys/app/features/__init__.py +3 -0
- chrys/app/features/buddy/__init__.py +3 -0
- chrys/app/features/buddy/actions.py +68 -0
- chrys/app/features/buddy/animation.py +76 -0
- chrys/app/features/buddy/commands.py +196 -0
- chrys/app/features/buddy/docs/design.md +187 -0
- chrys/app/features/buddy/hatchery.py +80 -0
- chrys/app/features/buddy/lifecycle.py +41 -0
- chrys/app/features/buddy/model.py +265 -0
- chrys/app/features/buddy/pixel_renderer.py +113 -0
- chrys/app/features/buddy/pixel_sprites.py +279 -0
- chrys/app/features/buddy/portrait.py +76 -0
- chrys/app/features/buddy/progression.py +58 -0
- chrys/app/features/buddy/replies.py +191 -0
- chrys/app/features/buddy/sprites/alien.toml +70 -0
- chrys/app/features/buddy/sprites/axolotl.toml +71 -0
- chrys/app/features/buddy/sprites/bat.toml +69 -0
- chrys/app/features/buddy/sprites/bee.toml +69 -0
- chrys/app/features/buddy/sprites/cactus.toml +73 -0
- chrys/app/features/buddy/sprites/capybara.toml +70 -0
- chrys/app/features/buddy/sprites/cat.toml +69 -0
- chrys/app/features/buddy/sprites/crab.toml +70 -0
- chrys/app/features/buddy/sprites/dragon.toml +71 -0
- chrys/app/features/buddy/sprites/duck.toml +69 -0
- chrys/app/features/buddy/sprites/fox.toml +69 -0
- chrys/app/features/buddy/sprites/frog.toml +70 -0
- chrys/app/features/buddy/sprites/ghost.toml +70 -0
- chrys/app/features/buddy/sprites/goose.toml +69 -0
- chrys/app/features/buddy/sprites/jelly.toml +70 -0
- chrys/app/features/buddy/sprites/llama.toml +70 -0
- chrys/app/features/buddy/sprites/mushroom.toml +69 -0
- chrys/app/features/buddy/sprites/octopus.toml +70 -0
- chrys/app/features/buddy/sprites/owl.toml +70 -0
- chrys/app/features/buddy/sprites/panda.toml +71 -0
- chrys/app/features/buddy/sprites/penguin.toml +69 -0
- chrys/app/features/buddy/sprites/rabbit.toml +70 -0
- chrys/app/features/buddy/sprites/robot.toml +72 -0
- chrys/app/features/buddy/sprites/shark.toml +70 -0
- chrys/app/features/buddy/sprites/snail.toml +69 -0
- chrys/app/features/buddy/sprites/snake.toml +69 -0
- chrys/app/features/buddy/sprites/tabby.toml +69 -0
- chrys/app/features/buddy/sprites/turtle.toml +69 -0
- chrys/app/features/buddy/store.py +143 -0
- chrys/app/features/session_title/__init__.py +7 -0
- chrys/app/features/session_title/updater.py +582 -0
- chrys/app/installer.py +651 -0
- chrys/app/parsing.py +17 -0
- chrys/app/tui/__init__.py +3 -0
- chrys/app/tui/app.py +1287 -0
- chrys/app/tui/behaviors/__init__.py +3 -0
- chrys/app/tui/behaviors/click_outside_dismiss.py +90 -0
- chrys/app/tui/behaviors/insert_clipboard.py +73 -0
- chrys/app/tui/behaviors/right_click_copy.py +146 -0
- chrys/app/tui/binding_display.py +139 -0
- chrys/app/tui/buddy_messages.py +17 -0
- chrys/app/tui/buddy_reply.py +86 -0
- chrys/app/tui/chrys.tcss +549 -0
- chrys/app/tui/clipboard.py +109 -0
- chrys/app/tui/copy_messages.py +10 -0
- chrys/app/tui/i18n.py +230 -0
- chrys/app/tui/language.py +58 -0
- chrys/app/tui/notifications/__init__.py +12 -0
- chrys/app/tui/notifications/drivers.py +180 -0
- chrys/app/tui/notifications/service.py +126 -0
- chrys/app/tui/notifications/settings.py +141 -0
- chrys/app/tui/screens/__init__.py +3 -0
- chrys/app/tui/screens/agents/__init__.py +8 -0
- chrys/app/tui/screens/agents/agent_draft_store.py +937 -0
- chrys/app/tui/screens/agents/config.py +2142 -0
- chrys/app/tui/screens/agents/config.tcss +169 -0
- chrys/app/tui/screens/agents/panels/__init__.py +3 -0
- chrys/app/tui/screens/agents/panels/acp.py +1176 -0
- chrys/app/tui/screens/agents/panels/basic.py +497 -0
- chrys/app/tui/screens/agents/panels/compaction.py +231 -0
- chrys/app/tui/screens/agents/panels/config_card.py +218 -0
- chrys/app/tui/screens/agents/panels/instructions.py +91 -0
- chrys/app/tui/screens/agents/panels/markdown_report.py +76 -0
- chrys/app/tui/screens/agents/panels/mcp.py +2262 -0
- chrys/app/tui/screens/agents/panels/memory.py +684 -0
- chrys/app/tui/screens/agents/panels/path_entry.py +227 -0
- chrys/app/tui/screens/agents/panels/skills.py +806 -0
- chrys/app/tui/screens/agents/panels/subagents.py +652 -0
- chrys/app/tui/screens/agents/panels/tools.py +205 -0
- chrys/app/tui/screens/agents/picker.py +103 -0
- chrys/app/tui/screens/agents/skill_paths.py +38 -0
- chrys/app/tui/screens/agents/validation_messages.py +452 -0
- chrys/app/tui/screens/dialogs/__init__.py +35 -0
- chrys/app/tui/screens/dialogs/agent_load.py +834 -0
- chrys/app/tui/screens/dialogs/agent_load.tcss +78 -0
- chrys/app/tui/screens/dialogs/app_mode.py +55 -0
- chrys/app/tui/screens/dialogs/approval/__init__.py +15 -0
- chrys/app/tui/screens/dialogs/approval/approval.tcss +311 -0
- chrys/app/tui/screens/dialogs/approval/bodies/__init__.py +3 -0
- chrys/app/tui/screens/dialogs/approval/bodies/file_edit.py +210 -0
- chrys/app/tui/screens/dialogs/approval/bodies/sub_agent.py +82 -0
- chrys/app/tui/screens/dialogs/approval/body.py +110 -0
- chrys/app/tui/screens/dialogs/approval/dialog.py +565 -0
- chrys/app/tui/screens/dialogs/approval/mode.py +105 -0
- chrys/app/tui/screens/dialogs/ask_user.py +148 -0
- chrys/app/tui/screens/dialogs/ask_user.tcss +62 -0
- chrys/app/tui/screens/dialogs/base.py +84 -0
- chrys/app/tui/screens/dialogs/confirm.py +184 -0
- chrys/app/tui/screens/dialogs/confirm.tcss +46 -0
- chrys/app/tui/screens/dialogs/connection_test.py +183 -0
- chrys/app/tui/screens/dialogs/connection_test.tcss +92 -0
- chrys/app/tui/screens/dialogs/diagram.py +102 -0
- chrys/app/tui/screens/dialogs/diagram.tcss +24 -0
- chrys/app/tui/screens/dialogs/editor.py +378 -0
- chrys/app/tui/screens/dialogs/editor.tcss +132 -0
- chrys/app/tui/screens/dialogs/file_picker.py +582 -0
- chrys/app/tui/screens/dialogs/file_picker.tcss +78 -0
- chrys/app/tui/screens/dialogs/fork_session.py +219 -0
- chrys/app/tui/screens/dialogs/fork_session.tcss +75 -0
- chrys/app/tui/screens/dialogs/image_compression.py +102 -0
- chrys/app/tui/screens/dialogs/image_compression.tcss +28 -0
- chrys/app/tui/screens/dialogs/man_page.py +172 -0
- chrys/app/tui/screens/dialogs/man_page.tcss +39 -0
- chrys/app/tui/screens/dialogs/new_folder.py +277 -0
- chrys/app/tui/screens/dialogs/new_folder.tcss +51 -0
- chrys/app/tui/screens/dialogs/notice.tcss +14 -0
- chrys/app/tui/screens/dialogs/prompt_cache.py +144 -0
- chrys/app/tui/screens/dialogs/prompt_cache.tcss +38 -0
- chrys/app/tui/screens/dialogs/runtime_details.py +371 -0
- chrys/app/tui/screens/dialogs/runtime_details.tcss +49 -0
- chrys/app/tui/screens/dialogs/session_title.py +110 -0
- chrys/app/tui/screens/dialogs/session_title.tcss +43 -0
- chrys/app/tui/screens/dialogs/tool_view.py +207 -0
- chrys/app/tui/screens/dialogs/tool_view.tcss +43 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.py +122 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.tcss +35 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.py +91 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.tcss +17 -0
- chrys/app/tui/screens/dialogs/workflow_input.py +258 -0
- chrys/app/tui/screens/dialogs/workflow_input.tcss +73 -0
- chrys/app/tui/screens/dialogs/workflow_node.py +462 -0
- chrys/app/tui/screens/dialogs/workflow_node.tcss +41 -0
- chrys/app/tui/screens/dialogs/workflow_picker.py +46 -0
- chrys/app/tui/screens/dialogs/workflow_result.py +136 -0
- chrys/app/tui/screens/dialogs/workflow_result.tcss +14 -0
- chrys/app/tui/screens/diff/__init__.py +22 -0
- chrys/app/tui/screens/diff/rollback_modal.py +1381 -0
- chrys/app/tui/screens/diff/rollback_modal.tcss +248 -0
- chrys/app/tui/screens/diff/screen.py +1532 -0
- chrys/app/tui/screens/diff/screen.tcss +104 -0
- chrys/app/tui/screens/guides/__init__.py +7 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/en/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/en/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/en/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/en/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/en/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/en/reference/workflows.md +1006 -0
- chrys/app/tui/screens/guides/_docs/en/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/en/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/_docs/index.yaml +100 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/workflows.md +1005 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/index.py +215 -0
- chrys/app/tui/screens/guides/reader.py +173 -0
- chrys/app/tui/screens/guides/screen.py +442 -0
- chrys/app/tui/screens/guides/screen.tcss +52 -0
- chrys/app/tui/screens/logs/__init__.py +7 -0
- chrys/app/tui/screens/logs/screen.py +504 -0
- chrys/app/tui/screens/logs/screen.tcss +45 -0
- chrys/app/tui/screens/main/__init__.py +7 -0
- chrys/app/tui/screens/main/action_availability.py +30 -0
- chrys/app/tui/screens/main/buddy_command.py +143 -0
- chrys/app/tui/screens/main/commands.py +1073 -0
- chrys/app/tui/screens/main/config_actions.py +464 -0
- chrys/app/tui/screens/main/copy_actions.py +117 -0
- chrys/app/tui/screens/main/dialog_controllers.py +826 -0
- chrys/app/tui/screens/main/dialog_gateway.py +166 -0
- chrys/app/tui/screens/main/diff_controller.py +478 -0
- chrys/app/tui/screens/main/engine_read_model.py +184 -0
- chrys/app/tui/screens/main/event_handlers.py +2014 -0
- chrys/app/tui/screens/main/input_flow.py +241 -0
- chrys/app/tui/screens/main/live_diff.py +244 -0
- chrys/app/tui/screens/main/main_screen_presenter.py +295 -0
- chrys/app/tui/screens/main/model_indicator.py +217 -0
- chrys/app/tui/screens/main/navigation.py +343 -0
- chrys/app/tui/screens/main/ports.py +571 -0
- chrys/app/tui/screens/main/recent_dirs.py +123 -0
- chrys/app/tui/screens/main/rollback_controller.py +586 -0
- chrys/app/tui/screens/main/runtime_info.py +71 -0
- chrys/app/tui/screens/main/screen.py +2265 -0
- chrys/app/tui/screens/main/screen.tcss +21 -0
- chrys/app/tui/screens/main/session_handlers.py +1127 -0
- chrys/app/tui/screens/main/session_title.py +239 -0
- chrys/app/tui/screens/main/settings_coordinator.py +365 -0
- chrys/app/tui/screens/main/settings_persistence.py +184 -0
- chrys/app/tui/screens/main/shell_mode.py +112 -0
- chrys/app/tui/screens/main/state.py +289 -0
- chrys/app/tui/screens/main/submit_state.py +11 -0
- chrys/app/tui/screens/main/subscriptions.py +177 -0
- chrys/app/tui/screens/main/suggestions.py +982 -0
- chrys/app/tui/screens/main/tool_action_bridge.py +75 -0
- chrys/app/tui/screens/main/view_adapter.py +1567 -0
- chrys/app/tui/screens/main/workflow_browser.py +747 -0
- chrys/app/tui/screens/main/workflow_content.py +248 -0
- chrys/app/tui/screens/main/workflow_controller.py +240 -0
- chrys/app/tui/screens/main/workflow_feedback.py +137 -0
- chrys/app/tui/screens/main/workflow_flow.py +49 -0
- chrys/app/tui/screens/main/workflow_run_control.py +456 -0
- chrys/app/tui/screens/main/workflow_session_view.py +316 -0
- chrys/app/tui/screens/main/workspace_actions.py +272 -0
- chrys/app/tui/screens/main/workspace_branch.py +194 -0
- chrys/app/tui/screens/menus/__init__.py +8 -0
- chrys/app/tui/screens/menus/languages.py +114 -0
- chrys/app/tui/screens/models/__init__.py +7 -0
- chrys/app/tui/screens/models/picker.py +147 -0
- chrys/app/tui/screens/models/screen.py +2043 -0
- chrys/app/tui/screens/models/screen.tcss +255 -0
- chrys/app/tui/screens/sessions/__init__.py +8 -0
- chrys/app/tui/screens/sessions/presenter.py +586 -0
- chrys/app/tui/screens/sessions/screen.py +779 -0
- chrys/app/tui/screens/sessions/screen.tcss +127 -0
- chrys/app/tui/screens/settings/__init__.py +29 -0
- chrys/app/tui/screens/settings/danger.py +26 -0
- chrys/app/tui/screens/settings/dialog.py +281 -0
- chrys/app/tui/screens/settings/layout.py +519 -0
- chrys/app/tui/screens/settings/migrate_dialog.py +253 -0
- chrys/app/tui/screens/settings/panes/__init__.py +3 -0
- chrys/app/tui/screens/settings/panes/notifications.py +248 -0
- chrys/app/tui/screens/settings/panes/sessions.py +148 -0
- chrys/app/tui/screens/settings/ports.py +55 -0
- chrys/app/tui/screens/settings/provenance.py +102 -0
- chrys/app/tui/screens/settings/rows.py +546 -0
- chrys/app/tui/screens/settings/settings.tcss +406 -0
- chrys/app/tui/screens/themes/__init__.py +8 -0
- chrys/app/tui/screens/themes/dialogs.py +330 -0
- chrys/app/tui/screens/themes/editor.py +632 -0
- chrys/app/tui/screens/themes/editor.tcss +268 -0
- chrys/app/tui/screens/themes/messages.py +95 -0
- chrys/app/tui/screens/themes/palette.py +694 -0
- chrys/app/tui/screens/themes/panel.py +113 -0
- chrys/app/tui/screens/themes/picker.py +203 -0
- chrys/app/tui/screens/themes/save.py +191 -0
- chrys/app/tui/support/__init__.py +3 -0
- chrys/app/tui/support/file_manager.py +64 -0
- chrys/app/tui/support/gc_freeze.py +1114 -0
- chrys/app/tui/support/path_shortcuts.py +50 -0
- chrys/app/tui/support/prompt_history.py +298 -0
- chrys/app/tui/support/workspace_mru.py +476 -0
- chrys/app/tui/terminal/__init__.py +6 -0
- chrys/app/tui/terminal/_pty_backend.py +114 -0
- chrys/app/tui/terminal/emulator/__init__.py +24 -0
- chrys/app/tui/terminal/emulator/buffer.py +456 -0
- chrys/app/tui/terminal/emulator/charsets.py +113 -0
- chrys/app/tui/terminal/emulator/core.py +963 -0
- chrys/app/tui/terminal/emulator/input.py +342 -0
- chrys/app/tui/terminal/emulator/parser.py +227 -0
- chrys/app/tui/terminal/emulator/pen.py +158 -0
- chrys/app/tui/terminal/launcher.py +455 -0
- chrys/app/tui/terminal/leaked_reports.py +44 -0
- chrys/app/tui/terminal/panel.py +323 -0
- chrys/app/tui/terminal/pty_process.py +405 -0
- chrys/app/tui/terminal/rendering.py +151 -0
- chrys/app/tui/terminal/shell.py +273 -0
- chrys/app/tui/terminal/shell_integration.py +199 -0
- chrys/app/tui/terminal/title.py +224 -0
- chrys/app/tui/terminal/widget.py +654 -0
- chrys/app/tui/theme.py +422 -0
- chrys/app/tui/theme_loader.py +201 -0
- chrys/app/tui/theme_stylesheet.py +49 -0
- chrys/app/tui/themes/__init__.py +3 -0
- chrys/app/tui/themes/document.py +120 -0
- chrys/app/tui/themes/preview.py +170 -0
- chrys/app/tui/themes/store.py +176 -0
- chrys/app/tui/toasts.py +69 -0
- chrys/app/tui/util/__init__.py +3 -0
- chrys/app/tui/util/context_pressure.py +146 -0
- chrys/app/tui/util/debug_log.py +292 -0
- chrys/app/tui/util/diff_entries.py +64 -0
- chrys/app/tui/util/formatting.py +53 -0
- chrys/app/tui/util/git_branch.py +326 -0
- chrys/app/tui/util/invocation_progress.py +57 -0
- chrys/app/tui/util/logo.py +32 -0
- chrys/app/tui/util/message_gate.py +35 -0
- chrys/app/tui/util/mutation_sources.py +14 -0
- chrys/app/tui/util/picker_text.py +27 -0
- chrys/app/tui/util/removal.py +47 -0
- chrys/app/tui/util/rich_style.py +36 -0
- chrys/app/tui/util/source_text.py +12 -0
- chrys/app/tui/util/static_update.py +88 -0
- chrys/app/tui/util/todo_format.py +51 -0
- chrys/app/tui/util/visibility.py +214 -0
- chrys/app/tui/widgets/__init__.py +60 -0
- chrys/app/tui/widgets/ask_user_controls.py +652 -0
- chrys/app/tui/widgets/ask_user_markdown.py +17 -0
- chrys/app/tui/widgets/ask_user_prompt.py +606 -0
- chrys/app/tui/widgets/buttons.py +52 -0
- chrys/app/tui/widgets/chat/__init__.py +3 -0
- chrys/app/tui/widgets/chat/agent_transcript_surface.py +818 -0
- chrys/app/tui/widgets/chat/chrome.py +296 -0
- chrys/app/tui/widgets/chat/compaction_card.py +353 -0
- chrys/app/tui/widgets/chat/context_fold.py +77 -0
- chrys/app/tui/widgets/chat/file_snapshot.py +91 -0
- chrys/app/tui/widgets/chat/image_preview.py +214 -0
- chrys/app/tui/widgets/chat/live_renderer.py +383 -0
- chrys/app/tui/widgets/chat/messages.py +1039 -0
- chrys/app/tui/widgets/chat/panel.py +1769 -0
- chrys/app/tui/widgets/chat/ports.py +245 -0
- chrys/app/tui/widgets/chat/renderers/__init__.py +7 -0
- chrys/app/tui/widgets/chat/renderers/ask_user.py +747 -0
- chrys/app/tui/widgets/chat/renderers/execute.py +431 -0
- chrys/app/tui/widgets/chat/renderers/file_edit.py +925 -0
- chrys/app/tui/widgets/chat/renderers/hosted_code.py +83 -0
- chrys/app/tui/widgets/chat/renderers/hosted_generic.py +330 -0
- chrys/app/tui/widgets/chat/renderers/hosted_image.py +107 -0
- chrys/app/tui/widgets/chat/renderers/hosted_mcp.py +43 -0
- chrys/app/tui/widgets/chat/renderers/hosted_search.py +167 -0
- chrys/app/tui/widgets/chat/renderers/hosted_shell.py +132 -0
- chrys/app/tui/widgets/chat/renderers/read_file.py +399 -0
- chrys/app/tui/widgets/chat/renderers/search.py +348 -0
- chrys/app/tui/widgets/chat/renderers/skill.py +564 -0
- chrys/app/tui/widgets/chat/renderers/sleep.py +287 -0
- chrys/app/tui/widgets/chat/renderers/sub_agent.py +1385 -0
- chrys/app/tui/widgets/chat/renderers/todo.py +114 -0
- chrys/app/tui/widgets/chat/renderers/web.py +69 -0
- chrys/app/tui/widgets/chat/replay.py +1605 -0
- chrys/app/tui/widgets/chat/replay_mount.py +322 -0
- chrys/app/tui/widgets/chat/scroll_controller.py +579 -0
- chrys/app/tui/widgets/chat/selection_controller.py +435 -0
- chrys/app/tui/widgets/chat/session_json.py +508 -0
- chrys/app/tui/widgets/chat/toc_model.py +156 -0
- chrys/app/tui/widgets/chat/tool_call.py +2478 -0
- chrys/app/tui/widgets/chat/tool_registry.py +753 -0
- chrys/app/tui/widgets/chat/tool_renderers.py +247 -0
- chrys/app/tui/widgets/chat/tool_view_builders.py +260 -0
- chrys/app/tui/widgets/chat/transcript_reader.py +57 -0
- chrys/app/tui/widgets/checkbox.py +24 -0
- chrys/app/tui/widgets/chrome/__init__.py +3 -0
- chrys/app/tui/widgets/chrome/app_header.py +283 -0
- chrys/app/tui/widgets/chrome/commands.py +95 -0
- chrys/app/tui/widgets/chrome/file_index.py +68 -0
- chrys/app/tui/widgets/chrome/file_scanner.py +670 -0
- chrys/app/tui/widgets/chrome/footer.py +284 -0
- chrys/app/tui/widgets/chrome/image_paste.py +351 -0
- chrys/app/tui/widgets/chrome/input_bar.py +1299 -0
- chrys/app/tui/widgets/chrome/status_bar.py +911 -0
- chrys/app/tui/widgets/chrome/suggestion_list.py +652 -0
- chrys/app/tui/widgets/click_affordance.py +24 -0
- chrys/app/tui/widgets/color_picker/__init__.py +254 -0
- chrys/app/tui/widgets/color_picker/controls.py +171 -0
- chrys/app/tui/widgets/color_picker/model.py +89 -0
- chrys/app/tui/widgets/dialog_buttons.py +58 -0
- chrys/app/tui/widgets/dialog_scroll.py +24 -0
- chrys/app/tui/widgets/diff_view/__init__.py +20 -0
- chrys/app/tui/widgets/diff_view/cells.py +59 -0
- chrys/app/tui/widgets/diff_view/code.py +280 -0
- chrys/app/tui/widgets/diff_view/compute.py +286 -0
- chrys/app/tui/widgets/diff_view/gutter.py +161 -0
- chrys/app/tui/widgets/diff_view/inline.py +77 -0
- chrys/app/tui/widgets/diff_view/palette.py +87 -0
- chrys/app/tui/widgets/diff_view/protocols.py +24 -0
- chrys/app/tui/widgets/diff_view/rows.py +138 -0
- chrys/app/tui/widgets/diff_view/unified.py +267 -0
- chrys/app/tui/widgets/diff_view/widget.py +209 -0
- chrys/app/tui/widgets/editor/__init__.py +79 -0
- chrys/app/tui/widgets/editor/coordinates.py +30 -0
- chrys/app/tui/widgets/editor/highlighter.py +139 -0
- chrys/app/tui/widgets/editor/keymaps.py +357 -0
- chrys/app/tui/widgets/editor/types.py +216 -0
- chrys/app/tui/widgets/editor/vim.py +1195 -0
- chrys/app/tui/widgets/editor/widget.py +401 -0
- chrys/app/tui/widgets/file_preview.py +58 -0
- chrys/app/tui/widgets/hatch.py +138 -0
- chrys/app/tui/widgets/input.py +66 -0
- chrys/app/tui/widgets/loading.py +125 -0
- chrys/app/tui/widgets/markdown/__init__.py +25 -0
- chrys/app/tui/widgets/markdown/_utils.py +21 -0
- chrys/app/tui/widgets/markdown/anchors.py +71 -0
- chrys/app/tui/widgets/markdown/blocks.py +115 -0
- chrys/app/tui/widgets/markdown/copyable.py +163 -0
- chrys/app/tui/widgets/markdown/diagram/__init__.py +35 -0
- chrys/app/tui/widgets/markdown/diagram/canvas.py +409 -0
- chrys/app/tui/widgets/markdown/diagram/charts.py +52 -0
- chrys/app/tui/widgets/markdown/diagram/diagnostics.py +101 -0
- chrys/app/tui/widgets/markdown/diagram/geometry.py +46 -0
- chrys/app/tui/widgets/markdown/diagram/layout.py +494 -0
- chrys/app/tui/widgets/markdown/diagram/messages.py +18 -0
- chrys/app/tui/widgets/markdown/diagram/model.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/parser.py +173 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/charts.py +349 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/common.py +403 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/graphs.py +1072 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/planning.py +317 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schedule.py +298 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schemas.py +241 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/structure.py +221 -0
- chrys/app/tui/widgets/markdown/diagram/presentation.py +32 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/charts.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/common.py +73 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/planning.py +157 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/schedule.py +202 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/structure.py +77 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/xy.py +385 -0
- chrys/app/tui/widgets/markdown/diagram/router.py +651 -0
- chrys/app/tui/widgets/markdown/diagram/specs/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/specs/charts.py +94 -0
- chrys/app/tui/widgets/markdown/diagram/specs/planning.py +68 -0
- chrys/app/tui/widgets/markdown/diagram/specs/schedule.py +65 -0
- chrys/app/tui/widgets/markdown/diagram/specs/structure.py +23 -0
- chrys/app/tui/widgets/markdown/diagram/viewer.py +92 -0
- chrys/app/tui/widgets/markdown/links.py +47 -0
- chrys/app/tui/widgets/markdown/math/__init__.py +36 -0
- chrys/app/tui/widgets/markdown/math/layout.py +739 -0
- chrys/app/tui/widgets/markdown/math/markdown.py +291 -0
- chrys/app/tui/widgets/markdown/math/parser.py +325 -0
- chrys/app/tui/widgets/markdown/math/symbols.py +325 -0
- chrys/app/tui/widgets/markdown/navigator.py +80 -0
- chrys/app/tui/widgets/markdown/parser.py +1057 -0
- chrys/app/tui/widgets/markdown/stream.py +73 -0
- chrys/app/tui/widgets/markdown/toc.py +98 -0
- chrys/app/tui/widgets/markdown/viewer.py +146 -0
- chrys/app/tui/widgets/markdown/widget.py +2046 -0
- chrys/app/tui/widgets/marquee.py +101 -0
- chrys/app/tui/widgets/option_menu.py +82 -0
- chrys/app/tui/widgets/page_navigator.py +118 -0
- chrys/app/tui/widgets/select.py +196 -0
- chrys/app/tui/widgets/selection.py +59 -0
- chrys/app/tui/widgets/sidebar/__init__.py +3 -0
- chrys/app/tui/widgets/sidebar/buddy.py +367 -0
- chrys/app/tui/widgets/sidebar/context.py +482 -0
- chrys/app/tui/widgets/sidebar/debug.py +195 -0
- chrys/app/tui/widgets/sidebar/empty_state.py +11 -0
- chrys/app/tui/widgets/sidebar/panel.py +198 -0
- chrys/app/tui/widgets/sidebar/tasks.py +198 -0
- chrys/app/tui/widgets/sidebar/toc.py +166 -0
- chrys/app/tui/widgets/sidebar/toc_types.py +30 -0
- chrys/app/tui/widgets/syntax_theme.py +85 -0
- chrys/app/tui/widgets/text_area.py +258 -0
- chrys/app/tui/widgets/trajectory/__init__.py +8 -0
- chrys/app/tui/widgets/trajectory/chartkit.py +264 -0
- chrys/app/tui/widgets/trajectory/insights.py +1266 -0
- chrys/app/tui/widgets/trajectory/overview.py +1101 -0
- chrys/app/tui/widgets/trajectory/panel.py +983 -0
- chrys/app/tui/widgets/trajectory/presentation.py +311 -0
- chrys/app/tui/widgets/trajectory/session_info.py +124 -0
- chrys/app/tui/widgets/trajectory/text_view.py +124 -0
- chrys/app/tui/widgets/trajectory/timeline.py +380 -0
- chrys/app/tui/widgets/welcome.py +126 -0
- chrys/app/tui/widgets/workflow/__init__.py +5 -0
- chrys/app/tui/widgets/workflow/edges.py +89 -0
- chrys/app/tui/widgets/workflow/graph.py +869 -0
- chrys/app/tui/widgets/workflow/info.py +310 -0
- chrys/app/tui/widgets/workflow/node_view.py +27 -0
- chrys/app/tui/widgets/workflow/output.py +225 -0
- chrys/app/tui/widgets/workflow/panel.py +658 -0
- chrys/app/tui/widgets/workflow/projector.py +324 -0
- chrys/app/tui/widgets/workflow/records.py +69 -0
- chrys/app/tui/widgets/workflow/scrollbar.py +82 -0
- chrys/app/tui/widgets/workflow/selection.py +151 -0
- chrys/app/tui/widgets/workflow/source.py +65 -0
- chrys/app/tui/widgets/workflow/text.py +257 -0
- chrys/app/tui/widgets/workflow/values.py +317 -0
- chrys/foundation/__init__.py +0 -0
- chrys/foundation/branding.py +15 -0
- chrys/foundation/config/__init__.py +7 -0
- chrys/foundation/config/coercion.py +374 -0
- chrys/foundation/config/context.py +34 -0
- chrys/foundation/config/env_file.py +141 -0
- chrys/foundation/config/env_layers.py +370 -0
- chrys/foundation/config/migrations.py +521 -0
- chrys/foundation/config/notification_events.py +22 -0
- chrys/foundation/config/process_settings.py +332 -0
- chrys/foundation/config/runtime_pointer.py +133 -0
- chrys/foundation/config/settings.py +1952 -0
- chrys/foundation/config/settings_store.py +996 -0
- chrys/foundation/config/spec.py +396 -0
- chrys/foundation/config/user_settings.py +140 -0
- chrys/foundation/config/warnings.py +262 -0
- chrys/foundation/config/web_values.py +135 -0
- chrys/foundation/config/yaml_store.py +487 -0
- chrys/foundation/errors/__init__.py +68 -0
- chrys/foundation/errors/_legacy.py +117 -0
- chrys/foundation/errors/_text.py +13 -0
- chrys/foundation/errors/_walk.py +85 -0
- chrys/foundation/errors/classify.py +278 -0
- chrys/foundation/errors/display.py +274 -0
- chrys/foundation/errors/formatting.py +283 -0
- chrys/foundation/errors/kinds.py +67 -0
- chrys/foundation/errors/network.py +497 -0
- chrys/foundation/errors/provider.py +445 -0
- chrys/foundation/errors/route.py +85 -0
- chrys/foundation/events/__init__.py +8 -0
- chrys/foundation/events/bus.py +143 -0
- chrys/foundation/events/types.py +1629 -0
- chrys/foundation/events/workflow.py +38 -0
- chrys/foundation/hosted_tools.py +142 -0
- chrys/foundation/i18n/__init__.py +32 -0
- chrys/foundation/i18n/_catalogs/zh-Hans/LC_MESSAGES/chrys.mo +0 -0
- chrys/foundation/i18n/catalogs.py +42 -0
- chrys/foundation/i18n/formatting.py +202 -0
- chrys/foundation/i18n/locale.py +92 -0
- chrys/foundation/i18n/localizer.py +187 -0
- chrys/foundation/i18n/messages.py +268 -0
- chrys/foundation/io/__init__.py +3 -0
- chrys/foundation/io/result_content.py +237 -0
- chrys/foundation/models/__init__.py +3 -0
- chrys/foundation/models/approval_reuse.py +23 -0
- chrys/foundation/models/ask_user.py +468 -0
- chrys/foundation/models/execution.py +18 -0
- chrys/foundation/models/history_markers.py +115 -0
- chrys/foundation/models/invocations.py +63 -0
- chrys/foundation/models/mutation_scope.py +28 -0
- chrys/foundation/models/session_env.py +87 -0
- chrys/foundation/models/session_surface.py +29 -0
- chrys/foundation/models/todos.py +74 -0
- chrys/foundation/models/turns.py +125 -0
- chrys/foundation/models/workflow_session.py +89 -0
- chrys/foundation/models/workspace.py +59 -0
- chrys/foundation/net/__init__.py +3 -0
- chrys/foundation/net/address_class.py +57 -0
- chrys/foundation/net/route_probe.py +94 -0
- chrys/foundation/net/url.py +107 -0
- chrys/foundation/observability/__init__.py +3 -0
- chrys/foundation/observability/exporters.py +174 -0
- chrys/foundation/observability/gate.py +31 -0
- chrys/foundation/observability/setup.py +497 -0
- chrys/foundation/observability/sink.py +228 -0
- chrys/foundation/patches/__init__.py +19 -0
- chrys/foundation/patches/mdurl_cache.py +59 -0
- chrys/foundation/patches/patcher.py +287 -0
- chrys/foundation/patches/staged_members.py +205 -0
- chrys/foundation/patches/textual_block_border.py +105 -0
- chrys/foundation/patches/textual_button_ansi.py +65 -0
- chrys/foundation/patches/textual_button_ansi_old.txt +68 -0
- chrys/foundation/patches/textual_callback_cache.py +248 -0
- chrys/foundation/patches/textual_callback_dispatch.py +109 -0
- chrys/foundation/patches/textual_compositor_cjk.py +566 -0
- chrys/foundation/patches/textual_dispatch_cache.py +150 -0
- chrys/foundation/patches/textual_ime_cursor_anchor.py +92 -0
- chrys/foundation/patches/textual_kitty_keyboard.py +268 -0
- chrys/foundation/patches/textual_lru_acyclic.py +241 -0
- chrys/foundation/patches/textual_message_pump.py +136 -0
- chrys/foundation/patches/textual_node_diet.py +875 -0
- chrys/foundation/patches/textual_one_shot_timer.py +48 -0
- chrys/foundation/patches/textual_option_list.py +90 -0
- chrys/foundation/patches/textual_precompose.py +89 -0
- chrys/foundation/patches/textual_pruned_tabs.py +69 -0
- chrys/foundation/patches/textual_reflow_reuse.py +1753 -0
- chrys/foundation/patches/textual_removed_node_caches.py +86 -0
- chrys/foundation/patches/textual_removed_screen_callbacks.py +55 -0
- chrys/foundation/patches/textual_selection_extract.py +48 -0
- chrys/foundation/patches/textual_strip_cycles.py +119 -0
- chrys/foundation/patches/textual_tab_selection.py +1611 -0
- chrys/foundation/patches/textual_timer_skip.py +70 -0
- chrys/foundation/patches/textual_utf8_decoder.py +106 -0
- chrys/foundation/patches/textual_win_sleep.py +167 -0
- chrys/foundation/patches/watchdog_fsevents.py +70 -0
- chrys/foundation/patches/watchdog_windows.py +52 -0
- chrys/foundation/platform/__init__.py +556 -0
- chrys/foundation/platform/c_api.py +32 -0
- chrys/foundation/platform/child_reap.py +107 -0
- chrys/foundation/platform/command_line.py +95 -0
- chrys/foundation/platform/files.py +1470 -0
- chrys/foundation/platform/output_capture.py +113 -0
- chrys/foundation/platform/paths.py +193 -0
- chrys/foundation/platform/process.py +1468 -0
- chrys/foundation/platform/pty_output.py +19 -0
- chrys/foundation/platform/runtime_paths.py +281 -0
- chrys/foundation/platform/win_console.py +129 -0
- chrys/foundation/reasoning_origin.py +67 -0
- chrys/foundation/recovery.py +16 -0
- chrys/foundation/retry.py +278 -0
- chrys/foundation/text/__init__.py +3 -0
- chrys/foundation/text/encoding.py +603 -0
- chrys/foundation/text/images.py +304 -0
- chrys/foundation/text/lines.py +24 -0
- chrys/foundation/text/mentions.py +90 -0
- chrys/foundation/text/model_json.py +25 -0
- chrys/foundation/text/reading_order.py +20 -0
- chrys/foundation/text/tokenizer.py +25 -0
- chrys/foundation/text/tool_output.py +287 -0
- chrys/foundation/text/yaml_io.py +62 -0
- chrys/foundation/tool_call_context.py +202 -0
- chrys/foundation/tool_execution_stamp.py +184 -0
- chrys/foundation/tool_invocation_order.py +48 -0
- chrys/foundation/tool_kinds.py +102 -0
- chrys/foundation/tool_result_metadata.py +287 -0
- chrys/foundation/trajectory/__init__.py +10 -0
- chrys/foundation/trajectory/context.py +481 -0
- chrys/foundation/trajectory/envelope.py +571 -0
- chrys/foundation/trajectory/event_types.py +294 -0
- chrys/foundation/trajectory/finalizers.py +46 -0
- chrys/foundation/trajectory/fingerprint.py +62 -0
- chrys/foundation/trajectory/ids.py +155 -0
- chrys/foundation/trajectory/keys.py +99 -0
- chrys/foundation/trajectory/lease.py +173 -0
- chrys/foundation/trajectory/metadata.py +72 -0
- chrys/foundation/trajectory/reader.py +271 -0
- chrys/foundation/trajectory/recovery.py +225 -0
- chrys/foundation/trajectory/revisions.py +152 -0
- chrys/foundation/trajectory/segments.py +225 -0
- chrys/foundation/trajectory/tools.py +117 -0
- chrys/foundation/trajectory/usage.py +151 -0
- chrys/foundation/trajectory/writer.py +972 -0
- chrys/foundation/trajectory_timing.py +97 -0
- chrys/foundation/util/__init__.py +3 -0
- chrys/foundation/util/async_tasks.py +29 -0
- chrys/foundation/util/chrys_headers.py +23 -0
- chrys/foundation/util/env_templates.py +104 -0
- chrys/foundation/util/filenames.py +96 -0
- chrys/foundation/util/header_charset.py +138 -0
- chrys/foundation/util/httpx_helpers.py +12 -0
- chrys/foundation/util/lock.py +236 -0
- chrys/foundation/util/once_close.py +46 -0
- chrys/foundation/util/session_ids.py +29 -0
- chrys/foundation/util/shell_tokens.py +88 -0
- chrys/foundation/util/sub_agent_context.py +38 -0
- chrys/foundation/util/time.py +35 -0
- chrys/foundation/util/unicode_scalars.py +36 -0
- chrys/foundation/vendor/__init__.py +43 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-unknown-linux-gnu +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-unknown-linux-musl +0 -0
- chrys/kernel/__init__.py +256 -0
- chrys/kernel/_content.py +1866 -0
- chrys/kernel/_loop_recorder.py +631 -0
- chrys/kernel/_null_overlay.py +1026 -0
- chrys/kernel/_result_ceiling.py +216 -0
- chrys/kernel/_serialization.py +431 -0
- chrys/kernel/_tool_arg_errors.py +1629 -0
- chrys/kernel/_tool_execution.py +1172 -0
- chrys/kernel/_tool_expansion.py +26 -0
- chrys/kernel/_types.py +2598 -0
- chrys/kernel/agent.py +994 -0
- chrys/kernel/client.py +1084 -0
- chrys/kernel/compaction.py +1315 -0
- chrys/kernel/exceptions.py +211 -0
- chrys/kernel/exchanges.py +683 -0
- chrys/kernel/identity.py +95 -0
- chrys/kernel/images.py +36 -0
- chrys/kernel/instrumentation.py +1618 -0
- chrys/kernel/loop.py +2308 -0
- chrys/kernel/middleware.py +616 -0
- chrys/kernel/sessions.py +604 -0
- chrys/kernel/tools.py +1061 -0
- chrys/kernel/types.py +126 -0
- chrys/orchestration/__init__.py +3 -0
- chrys/orchestration/engine/__init__.py +3 -0
- chrys/orchestration/engine/assembly.py +236 -0
- chrys/orchestration/engine/build/__init__.py +3 -0
- chrys/orchestration/engine/build/builder.py +1061 -0
- chrys/orchestration/engine/build/construction.py +408 -0
- chrys/orchestration/engine/build/loaded.py +139 -0
- chrys/orchestration/engine/engine.py +789 -0
- chrys/orchestration/engine/execution.py +771 -0
- chrys/orchestration/engine/loader.py +865 -0
- chrys/orchestration/engine/rollback.py +918 -0
- chrys/orchestration/engine/run/__init__.py +3 -0
- chrys/orchestration/engine/run/active_injection.py +419 -0
- chrys/orchestration/engine/run/attachments.py +519 -0
- chrys/orchestration/engine/run/bindings.py +1095 -0
- chrys/orchestration/engine/run/coordinator.py +1062 -0
- chrys/orchestration/engine/run/finalizer.py +417 -0
- chrys/orchestration/engine/run/input_refs.py +60 -0
- chrys/orchestration/engine/run/prompt_content.py +457 -0
- chrys/orchestration/engine/run/resume.py +299 -0
- chrys/orchestration/engine/run/retry.py +948 -0
- chrys/orchestration/engine/run/runner.py +625 -0
- chrys/orchestration/engine/run/runtime_skills.py +146 -0
- chrys/orchestration/engine/run/sub_agent_coordination.py +126 -0
- chrys/orchestration/engine/run/turn_hooks.py +258 -0
- chrys/orchestration/engine/run/turn_state.py +149 -0
- chrys/orchestration/engine/run/working_dir.py +50 -0
- chrys/orchestration/engine/session_lifecycle.py +2013 -0
- chrys/orchestration/engine/state/__init__.py +3 -0
- chrys/orchestration/engine/state/active_session.py +183 -0
- chrys/orchestration/engine/state/controls.py +639 -0
- chrys/orchestration/engine/state/current_agent.py +26 -0
- chrys/orchestration/engine/state/lifecycle_permits.py +395 -0
- chrys/orchestration/engine/state/machine.py +193 -0
- chrys/orchestration/engine/state/session_writer.py +434 -0
- chrys/orchestration/engine/trajectory.py +714 -0
- chrys/orchestration/engine/usage.py +118 -0
- chrys/orchestration/invoker/__init__.py +3 -0
- chrys/orchestration/invoker/acp.py +727 -0
- chrys/orchestration/invoker/acp_protocol.py +1115 -0
- chrys/orchestration/invoker/acp_spec.py +79 -0
- chrys/orchestration/invoker/attempts.py +994 -0
- chrys/orchestration/invoker/child_compaction.py +131 -0
- chrys/orchestration/invoker/child_history.py +99 -0
- chrys/orchestration/invoker/contracts.py +253 -0
- chrys/orchestration/invoker/evidence.py +94 -0
- chrys/orchestration/invoker/kernel.py +343 -0
- chrys/orchestration/invoker/origin.py +79 -0
- chrys/orchestration/invoker/resources.py +343 -0
- chrys/orchestration/invoker/runtime.py +299 -0
- chrys/orchestration/invoker/usage.py +80 -0
- chrys/orchestration/session_hooks.py +117 -0
- chrys/orchestration/session_host.py +1023 -0
- chrys/orchestration/session_resources.py +95 -0
- chrys/orchestration/session_usage.py +219 -0
- chrys/orchestration/startup.py +255 -0
- chrys/orchestration/sub_agents/__init__.py +3 -0
- chrys/orchestration/sub_agents/acp_policy.py +269 -0
- chrys/orchestration/sub_agents/kernel_policy.py +848 -0
- chrys/orchestration/sub_agents/registration.py +44 -0
- chrys/orchestration/sub_agents/shell.py +337 -0
- chrys/orchestration/sub_agents/tools.py +2228 -0
- chrys/orchestration/workflows/__init__.py +3 -0
- chrys/orchestration/workflows/agent_archive.py +156 -0
- chrys/orchestration/workflows/agent_node.py +894 -0
- chrys/orchestration/workflows/agent_node_build.py +687 -0
- chrys/orchestration/workflows/catalog.py +332 -0
- chrys/orchestration/workflows/coordinator.py +771 -0
- chrys/orchestration/workflows/hooks.py +176 -0
- chrys/orchestration/workflows/preview.py +322 -0
- chrys/orchestration/workflows/rollback.py +151 -0
- chrys/orchestration/workflows/runner.py +986 -0
- chrys/orchestration/workflows/session.py +390 -0
- chrys/orchestration/workflows/settings.py +97 -0
- chrys/orchestration/workflows/validation.py +674 -0
- chrys/orchestration/workflows/worker_client.py +1002 -0
- chrys/service/__init__.py +3 -0
- chrys/service/acp_client/__init__.py +65 -0
- chrys/service/acp_client/_transport.py +922 -0
- chrys/service/acp_client/client.py +1311 -0
- chrys/service/acp_client/errors.py +224 -0
- chrys/service/acp_client/protocol.py +220 -0
- chrys/service/acp_client/spawn.py +239 -0
- chrys/service/acp_client/spec.py +100 -0
- chrys/service/agent_middleware/__init__.py +60 -0
- chrys/service/agent_middleware/_metadata_keys.py +52 -0
- chrys/service/agent_middleware/control/__init__.py +3 -0
- chrys/service/agent_middleware/control/approval.py +769 -0
- chrys/service/agent_middleware/control/approval_hooks.py +62 -0
- chrys/service/agent_middleware/control/ask_user.py +207 -0
- chrys/service/agent_middleware/control/interrupt.py +50 -0
- chrys/service/agent_middleware/control/sleep.py +118 -0
- chrys/service/agent_middleware/control/todo.py +117 -0
- chrys/service/agent_middleware/events/__init__.py +3 -0
- chrys/service/agent_middleware/events/hook_dispatch.py +250 -0
- chrys/service/agent_middleware/events/hosted_tools.py +1762 -0
- chrys/service/agent_middleware/events/intermediate_text.py +128 -0
- chrys/service/agent_middleware/events/rejection_metadata.py +50 -0
- chrys/service/agent_middleware/events/result_persistence.py +98 -0
- chrys/service/agent_middleware/events/sub_agent_events.py +951 -0
- chrys/service/agent_middleware/events/tool_error_text.py +22 -0
- chrys/service/agent_middleware/events/tool_events.py +779 -0
- chrys/service/agent_middleware/injection.py +510 -0
- chrys/service/agent_middleware/reminders/__init__.py +91 -0
- chrys/service/agent_middleware/reminders/archive_pointer.py +154 -0
- chrys/service/agent_middleware/reminders/base.py +25 -0
- chrys/service/agent_middleware/reminders/context_usage.py +74 -0
- chrys/service/agent_middleware/reminders/file_change.py +63 -0
- chrys/service/agent_middleware/reminders/mcp.py +39 -0
- chrys/service/agent_middleware/reminders/profile_switch.py +106 -0
- chrys/service/agent_middleware/reminders/runtime_env.py +190 -0
- chrys/service/agent_middleware/reminders/skills.py +44 -0
- chrys/service/agent_middleware/reminders/sub_agents.py +35 -0
- chrys/service/agent_middleware/reminders/todo.py +35 -0
- chrys/service/agent_middleware/reminders/turn_line.py +48 -0
- chrys/service/agent_middleware/response_validation.py +1380 -0
- chrys/service/agent_middleware/system_reminder.py +1107 -0
- chrys/service/agent_middleware/validators.py +396 -0
- chrys/service/analytics/__init__.py +124 -0
- chrys/service/analytics/_actions.py +896 -0
- chrys/service/analytics/_context_evidence.py +357 -0
- chrys/service/analytics/_critical_path.py +179 -0
- chrys/service/analytics/_facts.py +919 -0
- chrys/service/analytics/_insights.py +769 -0
- chrys/service/analytics/_metric_ops.py +83 -0
- chrys/service/analytics/_session_projection.py +460 -0
- chrys/service/analytics/_timeline.py +434 -0
- chrys/service/analytics/_turn_graph.py +1237 -0
- chrys/service/analytics/_turns.py +1377 -0
- chrys/service/analytics/_workflows.py +171 -0
- chrys/service/analytics/aggregation.py +1474 -0
- chrys/service/analytics/classification.py +128 -0
- chrys/service/analytics/export.py +803 -0
- chrys/service/analytics/findings.py +282 -0
- chrys/service/analytics/math.py +79 -0
- chrys/service/analytics/model.py +886 -0
- chrys/service/analytics/reader.py +321 -0
- chrys/service/approval/__init__.py +3 -0
- chrys/service/approval/arbitration.py +102 -0
- chrys/service/approval/command_identity.py +109 -0
- chrys/service/approval/correlation.py +57 -0
- chrys/service/approval/grant_store.py +108 -0
- chrys/service/approval/judge.py +589 -0
- chrys/service/approval/level.py +15 -0
- chrys/service/approval/policy.py +209 -0
- chrys/service/approval/reuse.py +223 -0
- chrys/service/approval/reuse_binding.py +133 -0
- chrys/service/approval/safety_classifier.py +536 -0
- chrys/service/approval/turn_context.py +49 -0
- chrys/service/context/__init__.py +3 -0
- chrys/service/context/compaction/__init__.py +38 -0
- chrys/service/context/compaction/breaker.py +156 -0
- chrys/service/context/compaction/budgets.py +93 -0
- chrys/service/context/compaction/calibration.py +77 -0
- chrys/service/context/compaction/compression.py +690 -0
- chrys/service/context/compaction/current_turn_drop.py +458 -0
- chrys/service/context/compaction/events.py +201 -0
- chrys/service/context/compaction/exclusions.py +261 -0
- chrys/service/context/compaction/groups.py +361 -0
- chrys/service/context/compaction/last_words.py +1996 -0
- chrys/service/context/compaction/last_words_state.py +752 -0
- chrys/service/context/compaction/scoped.py +472 -0
- chrys/service/context/compaction/spill.py +1947 -0
- chrys/service/context/compaction/strategy.py +1116 -0
- chrys/service/context/compaction/summaries.py +319 -0
- chrys/service/context/compaction/turns.py +375 -0
- chrys/service/context/manager.py +174 -0
- chrys/service/context/memory_loader.py +792 -0
- chrys/service/context/middleware/__init__.py +3 -0
- chrys/service/context/middleware/usage.py +332 -0
- chrys/service/context/providers/__init__.py +3 -0
- chrys/service/context/providers/context_management.py +432 -0
- chrys/service/context/providers/history.py +647 -0
- chrys/service/context/providers/memory.py +50 -0
- chrys/service/context/tool_content.py +22 -0
- chrys/service/hooks/__init__.py +37 -0
- chrys/service/hooks/detached_worker.py +118 -0
- chrys/service/hooks/docs/README.md +14 -0
- chrys/service/hooks/docs/authoring.md +684 -0
- chrys/service/hooks/docs/configuration.md +499 -0
- chrys/service/hooks/docs/design.md +359 -0
- chrys/service/hooks/docs/project-hooks.md +348 -0
- chrys/service/hooks/events.py +157 -0
- chrys/service/hooks/loader.py +665 -0
- chrys/service/hooks/manager.py +807 -0
- chrys/service/hooks/matcher.py +111 -0
- chrys/service/hooks/outbox.py +294 -0
- chrys/service/hooks/runner.py +572 -0
- chrys/service/hooks/schema.py +407 -0
- chrys/service/llm/__init__.py +7 -0
- chrys/service/llm/_structured_outputs.py +192 -0
- chrys/service/llm/anthropic_messages/__init__.py +9 -0
- chrys/service/llm/anthropic_messages/client.py +254 -0
- chrys/service/llm/anthropic_messages/decode.py +331 -0
- chrys/service/llm/anthropic_messages/history.py +279 -0
- chrys/service/llm/anthropic_messages/request.py +323 -0
- chrys/service/llm/anthropic_messages/server_tools.py +319 -0
- chrys/service/llm/anthropic_messages/stream.py +297 -0
- chrys/service/llm/anthropic_messages/thinking_binding.py +156 -0
- chrys/service/llm/chat_completions/__init__.py +9 -0
- chrys/service/llm/chat_completions/client.py +369 -0
- chrys/service/llm/chat_completions/decode.py +338 -0
- chrys/service/llm/chat_completions/history.py +657 -0
- chrys/service/llm/chat_completions/reasoning.py +244 -0
- chrys/service/llm/chat_completions/request.py +192 -0
- chrys/service/llm/chat_completions/stream.py +546 -0
- chrys/service/llm/chat_completions/validation.py +308 -0
- chrys/service/llm/clients.py +613 -0
- chrys/service/llm/images.py +65 -0
- chrys/service/llm/mock.py +413 -0
- chrys/service/llm/observer.py +446 -0
- chrys/service/llm/one_shot.py +106 -0
- chrys/service/llm/openai_exceptions.py +93 -0
- chrys/service/llm/openai_responses/__init__.py +10 -0
- chrys/service/llm/openai_responses/client.py +323 -0
- chrys/service/llm/openai_responses/decode.py +432 -0
- chrys/service/llm/openai_responses/history.py +519 -0
- chrys/service/llm/openai_responses/hosted.py +625 -0
- chrys/service/llm/openai_responses/replay.py +629 -0
- chrys/service/llm/openai_responses/request.py +342 -0
- chrys/service/llm/openai_responses/stream.py +847 -0
- chrys/service/llm/openai_timestamps.py +57 -0
- chrys/service/llm/providers.py +107 -0
- chrys/service/llm/proxy_route.py +54 -0
- chrys/service/llm/raw_http_log.py +309 -0
- chrys/service/llm/route_facts.py +63 -0
- chrys/service/llm/route_sessions.py +49 -0
- chrys/service/llm/sdk_anthropic.py +18 -0
- chrys/service/llm/sdk_headers.py +47 -0
- chrys/service/llm/sdk_openai.py +18 -0
- chrys/service/llm/sdk_retry.py +59 -0
- chrys/service/llm/wire_client.py +246 -0
- chrys/service/mcp/__init__.py +3 -0
- chrys/service/mcp/_connection.py +235 -0
- chrys/service/mcp/_http_transport.py +350 -0
- chrys/service/mcp/_progressive.py +459 -0
- chrys/service/mcp/_stdio_transport.py +614 -0
- chrys/service/mcp/_tool_mixins.py +96 -0
- chrys/service/mcp/adapter.py +1031 -0
- chrys/service/mcp/cache.py +476 -0
- chrys/service/mcp/content.py +32 -0
- chrys/service/mcp/errors.py +169 -0
- chrys/service/mcp/owned.py +1330 -0
- chrys/service/mcp/result_limits.py +168 -0
- chrys/service/mcp/thinking_warning.py +95 -0
- chrys/service/mcp/validation.py +74 -0
- chrys/service/mutations/__init__.py +84 -0
- chrys/service/mutations/coordination.py +894 -0
- chrys/service/mutations/detector.py +458 -0
- chrys/service/mutations/git_calibrator.py +397 -0
- chrys/service/mutations/git_state.py +647 -0
- chrys/service/mutations/pipeline.py +779 -0
- chrys/service/mutations/scanner.py +466 -0
- chrys/service/mutations/snapshot_files.py +233 -0
- chrys/service/mutations/store.py +625 -0
- chrys/service/mutations/tool_names.py +15 -0
- chrys/service/mutations/trace.py +246 -0
- chrys/service/mutations/tracker.py +1289 -0
- chrys/service/mutations/types.py +559 -0
- chrys/service/mutations/workspace_changes.py +1583 -0
- chrys/service/profiles/__init__.py +2 -0
- chrys/service/profiles/agents/__init__.py +3 -0
- chrys/service/profiles/agents/builtins/Code.yaml +199 -0
- chrys/service/profiles/agents/builtins/Explore.yaml +62 -0
- chrys/service/profiles/agents/builtins/General.yaml +114 -0
- chrys/service/profiles/agents/builtins/QA.yaml +128 -0
- chrys/service/profiles/agents/loader.py +808 -0
- chrys/service/profiles/agents/registry.py +727 -0
- chrys/service/profiles/agents/schema.py +371 -0
- chrys/service/profiles/agents/serializer.py +196 -0
- chrys/service/profiles/coercion.py +22 -0
- chrys/service/profiles/models/__init__.py +3 -0
- chrys/service/profiles/models/env_bridge.py +157 -0
- chrys/service/profiles/models/loader.py +282 -0
- chrys/service/profiles/models/options.py +438 -0
- chrys/service/profiles/models/registry.py +104 -0
- chrys/service/profiles/models/resolver.py +286 -0
- chrys/service/profiles/models/schema.py +91 -0
- chrys/service/profiles/models/serializer.py +76 -0
- chrys/service/session/__init__.py +3 -0
- chrys/service/session/checkpoint.py +156 -0
- chrys/service/session/history.py +1616 -0
- chrys/service/session/message_metadata.py +151 -0
- chrys/service/session/persistence.py +435 -0
- chrys/service/session/runtime_metadata.py +210 -0
- chrys/service/session/sub_agent_logs.py +1069 -0
- chrys/service/session/sub_agent_transcript.py +654 -0
- chrys/service/skills/__init__.py +3 -0
- chrys/service/skills/adapter.py +408 -0
- chrys/service/skills/constants.py +8 -0
- chrys/service/skills/loader.py +575 -0
- chrys/service/skills/model.py +130 -0
- chrys/service/skills/provider.py +841 -0
- chrys/service/skills/runner.py +390 -0
- chrys/service/state/__init__.py +3 -0
- chrys/service/state/_fork.py +710 -0
- chrys/service/state/_session_files.py +175 -0
- chrys/service/state/_session_meta.py +552 -0
- chrys/service/state/locks.py +154 -0
- chrys/service/state/serializers.py +156 -0
- chrys/service/state/session_catalog.py +361 -0
- chrys/service/state/session_listing.py +84 -0
- chrys/service/state/session_migration.py +446 -0
- chrys/service/state/session_mru.py +315 -0
- chrys/service/state/store.py +2132 -0
- chrys/service/state/workflow.py +158 -0
- chrys/service/text_blocks.py +47 -0
- chrys/service/todos/__init__.py +3 -0
- chrys/service/todos/reminder.py +31 -0
- chrys/service/todos/tracker.py +58 -0
- chrys/service/tools/__init__.py +3 -0
- chrys/service/tools/approval_targets.py +92 -0
- chrys/service/tools/builtins/__init__.py +3 -0
- chrys/service/tools/builtins/ask_user.py +326 -0
- chrys/service/tools/builtins/doc_converter/__init__.py +494 -0
- chrys/service/tools/builtins/doc_converter/artifacts.py +392 -0
- chrys/service/tools/builtins/doc_converter/parsers/__init__.py +3 -0
- chrys/service/tools/builtins/doc_converter/parsers/_table.py +34 -0
- chrys/service/tools/builtins/doc_converter/parsers/base.py +101 -0
- chrys/service/tools/builtins/doc_converter/parsers/docx.py +88 -0
- chrys/service/tools/builtins/doc_converter/parsers/pdf.py +144 -0
- chrys/service/tools/builtins/doc_converter/parsers/pptx.py +103 -0
- chrys/service/tools/builtins/doc_converter/parsers/xls.py +32 -0
- chrys/service/tools/builtins/doc_converter/parsers/xlsx.py +51 -0
- chrys/service/tools/builtins/doc_converter/registry.py +55 -0
- chrys/service/tools/builtins/filesystem.py +1071 -0
- chrys/service/tools/builtins/search.py +1265 -0
- chrys/service/tools/builtins/shell.py +967 -0
- chrys/service/tools/builtins/shell_command_filtering/__init__.py +5 -0
- chrys/service/tools/builtins/shell_command_filtering/policy.py +60 -0
- chrys/service/tools/builtins/shell_command_filtering/presets.py +152 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/__init__.py +358 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/commands.py +416 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/git.py +408 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/syntax.py +87 -0
- chrys/service/tools/builtins/shell_command_filtering/tokenizer.py +214 -0
- chrys/service/tools/builtins/shell_filter.py +24 -0
- chrys/service/tools/builtins/sleep.py +91 -0
- chrys/service/tools/builtins/todo.py +64 -0
- chrys/service/tools/builtins/web/__init__.py +6 -0
- chrys/service/tools/builtins/web/build.py +268 -0
- chrys/service/tools/builtins/web/config.py +338 -0
- chrys/service/tools/builtins/web/credentials.py +37 -0
- chrys/service/tools/builtins/web/fetch/__init__.py +3 -0
- chrys/service/tools/builtins/web/fetch/cache.py +68 -0
- chrys/service/tools/builtins/web/fetch/convert.py +56 -0
- chrys/service/tools/builtins/web/fetch/tool.py +223 -0
- chrys/service/tools/builtins/web/hosted.py +61 -0
- chrys/service/tools/builtins/web/http.py +330 -0
- chrys/service/tools/builtins/web/progress.py +18 -0
- chrys/service/tools/builtins/web/search/__init__.py +3 -0
- chrys/service/tools/builtins/web/search/exa_mcp.py +146 -0
- chrys/service/tools/builtins/web/search/html.py +152 -0
- chrys/service/tools/builtins/web/search/json_api.py +227 -0
- chrys/service/tools/builtins/web/search/providers.py +77 -0
- chrys/service/tools/builtins/web/search/tool.py +218 -0
- chrys/service/tools/builtins/web/search/types.py +28 -0
- chrys/service/tools/builtins/web/tls_identity.py +66 -0
- chrys/service/tools/kinds.py +115 -0
- chrys/service/tools/names.py +47 -0
- chrys/service/tools/registry.py +242 -0
- chrys/service/tools/result_metadata.py +103 -0
- chrys/service/tools/session_artifacts.py +196 -0
- chrys/service/tools/spill.py +218 -0
- chrys/service/tools/workspace_paths.py +81 -0
- chrys/service/trajectory/__init__.py +11 -0
- chrys/service/trajectory/approvals.py +201 -0
- chrys/service/trajectory/compaction.py +420 -0
- chrys/service/trajectory/fork.py +75 -0
- chrys/service/trajectory/hooks.py +217 -0
- chrys/service/trajectory/items.py +41 -0
- chrys/service/trajectory/preparation.py +374 -0
- chrys/service/trajectory/retries.py +95 -0
- chrys/service/trajectory/revisions.py +129 -0
- chrys/service/trajectory/segmented.py +163 -0
- chrys/service/trajectory/session.py +762 -0
- chrys/service/trajectory/state.py +83 -0
- chrys/service/trajectory/sub_agents.py +169 -0
- chrys/service/trajectory/tombstone.py +262 -0
- chrys/service/trajectory/tools.py +323 -0
- chrys/service/trajectory/validation.py +130 -0
- chrys/service/trajectory/waits.py +161 -0
- chrys/service/trajectory/workflow.py +148 -0
- chrys/service/vision.py +128 -0
- chrys/service/workflows/__init__.py +3 -0
- chrys/service/workflows/admission.py +151 -0
- chrys/service/workflows/artifacts.py +109 -0
- chrys/service/workflows/asks.py +130 -0
- chrys/service/workflows/builtins/__init__.py +12 -0
- chrys/service/workflows/builtins/demo-workflow.manifest.json +635 -0
- chrys/service/workflows/builtins/demo-workflow.py +756 -0
- chrys/service/workflows/discovery.py +554 -0
- chrys/service/workflows/environment.py +284 -0
- chrys/service/workflows/graph.py +430 -0
- chrys/service/workflows/history.py +82 -0
- chrys/service/workflows/interpreter.py +107 -0
- chrys/service/workflows/journal.py +334 -0
- chrys/service/workflows/layout.py +56 -0
- chrys/service/workflows/ledger.py +149 -0
- chrys/service/workflows/model_selection.py +18 -0
- chrys/service/workflows/orphans.py +209 -0
- chrys/service/workflows/outcomes.py +46 -0
- chrys/service/workflows/protocol.py +210 -0
- chrys/service/workflows/records.py +142 -0
- chrys/service/workflows/scheduler.py +770 -0
- chrys/service/workflows/sdk/__init__.py +51 -0
- chrys/service/workflows/sdk/_ask.py +167 -0
- chrys/service/workflows/sdk/_builder.py +781 -0
- chrys/service/workflows/sdk/_values.py +92 -0
- chrys/service/workflows/sdk_artifact.py +74 -0
- chrys/service/workflows/store.py +494 -0
- chrys/service/workflows/transcript.py +102 -0
- chrys/service/workflows/values.py +73 -0
- chrys/service/workflows/worker_host.py +1820 -0
- chrys/workflows.py +38 -0
- icode_tui-0.29.1.dist-info/METADATA +104 -0
- icode_tui-0.29.1.dist-info/RECORD +1138 -0
- icode_tui-0.29.1.dist-info/WHEEL +4 -0
- icode_tui-0.29.1.dist-info/entry_points.txt +2 -0
- icode_tui-0.29.1.dist-info/licenses/LICENSE +202 -0
- icode_tui-0.29.1.dist-info/licenses/NOTICE +1955 -0
chrys/kernel/_types.py
ADDED
|
@@ -0,0 +1,2598 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
3
|
+
# Contains code adapted from Microsoft Agent Framework (MIT License; see NOTICE).
|
|
4
|
+
|
|
5
|
+
"""Chrys-owned chat/agent types."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import contextlib
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from asyncio import iscoroutine
|
|
14
|
+
from collections.abc import (
|
|
15
|
+
AsyncIterable,
|
|
16
|
+
AsyncIterator,
|
|
17
|
+
Awaitable,
|
|
18
|
+
Callable,
|
|
19
|
+
Generator,
|
|
20
|
+
Iterable,
|
|
21
|
+
Mapping,
|
|
22
|
+
MutableMapping,
|
|
23
|
+
Sequence,
|
|
24
|
+
)
|
|
25
|
+
from copy import deepcopy
|
|
26
|
+
from inspect import isawaitable
|
|
27
|
+
from typing import (
|
|
28
|
+
TYPE_CHECKING,
|
|
29
|
+
Any,
|
|
30
|
+
ClassVar,
|
|
31
|
+
Final,
|
|
32
|
+
Generic,
|
|
33
|
+
Literal,
|
|
34
|
+
NewType,
|
|
35
|
+
ReadOnly,
|
|
36
|
+
TypeVar,
|
|
37
|
+
cast,
|
|
38
|
+
final,
|
|
39
|
+
overload,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
from pydantic import BaseModel
|
|
43
|
+
from typing_extensions import TypedDict
|
|
44
|
+
|
|
45
|
+
from ._content import (
|
|
46
|
+
Content,
|
|
47
|
+
UsageDetails,
|
|
48
|
+
_combine_annotations,
|
|
49
|
+
_combine_raw_representations,
|
|
50
|
+
_parse_content_list,
|
|
51
|
+
_restore_compaction_annotation_in_additional_properties,
|
|
52
|
+
_text_for_join,
|
|
53
|
+
add_usage_details,
|
|
54
|
+
normalize_stream_usage,
|
|
55
|
+
)
|
|
56
|
+
from ._serialization import SerializationMixin
|
|
57
|
+
from ._tool_expansion import _get_tool_expander
|
|
58
|
+
from .exceptions import AdditionItemMismatch, ContentError
|
|
59
|
+
from .identity import ContentList
|
|
60
|
+
|
|
61
|
+
if TYPE_CHECKING:
|
|
62
|
+
from .tools import FunctionTool as _OwnedFunctionTool
|
|
63
|
+
else:
|
|
64
|
+
_OwnedFunctionTool = object
|
|
65
|
+
|
|
66
|
+
type ToolTypes = _OwnedFunctionTool | Mapping[str, Any] | object
|
|
67
|
+
|
|
68
|
+
logger = logging.getLogger(__name__)
|
|
69
|
+
|
|
70
|
+
# Identity-only sentinel carried by synthetic stream-progress updates. It is
|
|
71
|
+
# deliberately opaque: rejected provider payloads must never ride heartbeat
|
|
72
|
+
# updates across response-validation boundaries.
|
|
73
|
+
# Keep this marker and the matching
|
|
74
|
+
# ``ChatResponseUpdate.transport_heartbeat`` / ``_process_update`` handling
|
|
75
|
+
# together when changing streaming types, so heartbeats remain distinguishable.
|
|
76
|
+
_STREAM_HEARTBEAT_MARKER = object()
|
|
77
|
+
RETRY_BOUNDARY_UPDATE_KEY = "_chrys_retry_boundary"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# region Content Parsing Utilities
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# endregion
|
|
84
|
+
|
|
85
|
+
# region Constants and types
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
_UNSET_USAGE: Final[UsageDetails] = cast("UsageDetails", object())
|
|
89
|
+
"""Sentinel distinguishing an omitted ``latest_usage_details`` from an explicit None.
|
|
90
|
+
|
|
91
|
+
An omitted argument means "single-call response" and inherits ``usage_details``.
|
|
92
|
+
An explicit ``None`` means the final call's usage is genuinely unavailable and
|
|
93
|
+
must NOT fall back to the aggregate — consumers such as after-run
|
|
94
|
+
force-compression would otherwise mistake turn-total billing usage for the
|
|
95
|
+
final call's context occupancy.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# region Content Class
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# endregion
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# region Chat Response constants
|
|
106
|
+
|
|
107
|
+
RoleLiteral = Literal["system", "user", "assistant", "tool"]
|
|
108
|
+
"""Literal type for known role values. Accepts any string for extensibility."""
|
|
109
|
+
|
|
110
|
+
Role = NewType("Role", str)
|
|
111
|
+
"""Type for chat message roles. Use string values directly (e.g., "user", "assistant").
|
|
112
|
+
|
|
113
|
+
Known values: "system", "user", "assistant", "tool"
|
|
114
|
+
|
|
115
|
+
Examples:
|
|
116
|
+
.. code-block:: python
|
|
117
|
+
|
|
118
|
+
from chrys.kernel._types import Message
|
|
119
|
+
|
|
120
|
+
# Use string values directly
|
|
121
|
+
user_msg = Message("user", ["Hello"])
|
|
122
|
+
assistant_msg = Message("assistant", ["Hi there!"])
|
|
123
|
+
|
|
124
|
+
# Custom roles are also supported
|
|
125
|
+
custom_msg = Message("custom", ["Custom role message"])
|
|
126
|
+
|
|
127
|
+
# Compare roles directly as strings
|
|
128
|
+
if user_msg.role == "user":
|
|
129
|
+
print("This is a user message")
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
FinishReasonLiteral = Literal["stop", "length", "tool_calls", "content_filter"]
|
|
133
|
+
"""Literal type for known finish reason values. Accepts any string for extensibility."""
|
|
134
|
+
|
|
135
|
+
FinishReason = NewType("FinishReason", str)
|
|
136
|
+
"""Type for chat response finish reasons. Use string values directly.
|
|
137
|
+
|
|
138
|
+
Known values:
|
|
139
|
+
- "stop": Normal completion
|
|
140
|
+
- "length": Max tokens reached
|
|
141
|
+
- "tool_calls": Tool calls triggered
|
|
142
|
+
- "content_filter": Content filter triggered
|
|
143
|
+
|
|
144
|
+
Examples:
|
|
145
|
+
.. code-block:: python
|
|
146
|
+
|
|
147
|
+
from chrys.kernel._types import ChatResponse
|
|
148
|
+
|
|
149
|
+
response = ChatResponse(messages=[...], finish_reason="stop")
|
|
150
|
+
|
|
151
|
+
# Check finish reason directly as string
|
|
152
|
+
if response.finish_reason == "stop":
|
|
153
|
+
print("Response completed normally")
|
|
154
|
+
elif response.finish_reason == "tool_calls":
|
|
155
|
+
print("Tool calls need to be processed")
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# region Message
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class Message(SerializationMixin):
|
|
163
|
+
"""Represents a chat message.
|
|
164
|
+
|
|
165
|
+
Attributes:
|
|
166
|
+
role: The role of the author of the message.
|
|
167
|
+
contents: The chat message content items.
|
|
168
|
+
author_name: The name of the author of the message.
|
|
169
|
+
message_id: The ID of the chat message.
|
|
170
|
+
additional_properties: Any additional properties associated with the chat message.
|
|
171
|
+
Additional properties are used internally, they are not sent to services.
|
|
172
|
+
raw_representation: The raw representation of the chat message from an underlying implementation.
|
|
173
|
+
|
|
174
|
+
Examples:
|
|
175
|
+
.. code-block:: python
|
|
176
|
+
|
|
177
|
+
from chrys.kernel._content import Content
|
|
178
|
+
from chrys.kernel._types import Message
|
|
179
|
+
|
|
180
|
+
# Create a message with text content
|
|
181
|
+
user_msg = Message("user", ["What's the weather?"])
|
|
182
|
+
print(user_msg.text) # "What's the weather?"
|
|
183
|
+
|
|
184
|
+
# Create a system message
|
|
185
|
+
system_msg = Message("system", ["You are a helpful assistant."])
|
|
186
|
+
|
|
187
|
+
# Create a message with mixed content types
|
|
188
|
+
assistant_msg = Message(
|
|
189
|
+
"assistant",
|
|
190
|
+
["The weather is sunny!", Content.from_image_uri("https://...")],
|
|
191
|
+
)
|
|
192
|
+
print(assistant_msg.text) # "The weather is sunny!"
|
|
193
|
+
|
|
194
|
+
# Serialization - to_dict and from_dict
|
|
195
|
+
msg_dict = user_msg.to_dict()
|
|
196
|
+
# {'type': 'chat_message', 'role': 'user',
|
|
197
|
+
# 'contents': [{'type': 'text', 'text': "What's the weather?"}], 'additional_properties': {}}
|
|
198
|
+
restored_msg = Message.from_dict(msg_dict)
|
|
199
|
+
print(restored_msg.text) # "What's the weather?"
|
|
200
|
+
|
|
201
|
+
# Serialization - to_json and from_json
|
|
202
|
+
msg_json = user_msg.to_json()
|
|
203
|
+
# '{"type": "chat_message", "role": "user", "contents": [...], ...}'
|
|
204
|
+
restored_from_json = Message.from_json(msg_json)
|
|
205
|
+
print(restored_from_json.role) # "user"
|
|
206
|
+
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"raw_representation"}
|
|
210
|
+
|
|
211
|
+
def __init__(
|
|
212
|
+
self,
|
|
213
|
+
role: RoleLiteral | str,
|
|
214
|
+
contents: Sequence[Content | str | Mapping[str, Any]] | None = None,
|
|
215
|
+
*,
|
|
216
|
+
author_name: str | None = None,
|
|
217
|
+
message_id: str | None = None,
|
|
218
|
+
additional_properties: MutableMapping[str, Any] | None = None,
|
|
219
|
+
raw_representation: Any | None = None,
|
|
220
|
+
) -> None:
|
|
221
|
+
"""Initialize Message.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
role: The role of the author of the message (e.g., "user", "assistant", "system", "tool").
|
|
225
|
+
contents: A sequence of content items. Can be Content objects, strings (auto-converted
|
|
226
|
+
to TextContent), or dicts (parsed via Content.from_dict). Defaults to empty list.
|
|
227
|
+
|
|
228
|
+
Keyword Args:
|
|
229
|
+
author_name: Optional name of the author of the message.
|
|
230
|
+
message_id: Optional ID of the chat message.
|
|
231
|
+
additional_properties: Optional additional properties associated with the chat message.
|
|
232
|
+
Additional properties are used internally, they are not sent to services.
|
|
233
|
+
raw_representation: Optional raw representation of the chat message.
|
|
234
|
+
"""
|
|
235
|
+
parsed_contents = [] if contents is None else _parse_content_list(contents)
|
|
236
|
+
|
|
237
|
+
self.role: str = role
|
|
238
|
+
self.contents = parsed_contents
|
|
239
|
+
self.author_name = author_name
|
|
240
|
+
self.message_id = message_id
|
|
241
|
+
self.additional_properties = (
|
|
242
|
+
_restore_compaction_annotation_in_additional_properties(additional_properties) or {}
|
|
243
|
+
)
|
|
244
|
+
self.raw_representation = raw_representation
|
|
245
|
+
# Runtime-only assembly provenance. The tool loop sets this when an
|
|
246
|
+
# update for this logical message had echoed content removed before
|
|
247
|
+
# assembly; private attributes are excluded from serialization.
|
|
248
|
+
self._chrys_echo_content_stripped = False
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def contents(self) -> ContentList:
|
|
252
|
+
"""The chat message content items, always a ``ContentList``.
|
|
253
|
+
|
|
254
|
+
The setter coerces any assigned sequence so captors can hold
|
|
255
|
+
``weakref.ref(msg.contents)`` (built-in ``list`` is not
|
|
256
|
+
weakref-able). It backs onto ``__dict__["contents"]`` — the PUBLIC
|
|
257
|
+
key — because ``to_dict`` enumerates ``__dict__`` dropping
|
|
258
|
+
``_``-prefixed keys and the generic ``__deepcopy__`` copies
|
|
259
|
+
``__dict__`` entries via ``object.__setattr__`` (which follows the
|
|
260
|
+
descriptor protocol back into this setter). ``ContentList`` input is
|
|
261
|
+
stored as-is, preserving list identity for shared-list twins and
|
|
262
|
+
deepcopy re-coercion; a plain list is copied into a fresh
|
|
263
|
+
``ContentList``, so a caller's plain-list alias no longer aliases
|
|
264
|
+
``msg.contents`` after assignment.
|
|
265
|
+
"""
|
|
266
|
+
return self.__dict__["contents"]
|
|
267
|
+
|
|
268
|
+
@contents.setter
|
|
269
|
+
def contents(self, value: Sequence[Content]) -> None:
|
|
270
|
+
if not isinstance(value, ContentList):
|
|
271
|
+
value = ContentList(value)
|
|
272
|
+
self.__dict__["contents"] = value
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def text(self) -> str:
|
|
276
|
+
"""Returns the text content of the message.
|
|
277
|
+
|
|
278
|
+
Remarks:
|
|
279
|
+
This property concatenates the text of all TextContent objects in Content.
|
|
280
|
+
"""
|
|
281
|
+
return " ".join(_text_for_join(content) for content in self.contents if content.type == "text")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
AgentRunInputs = str | Content | Message | Sequence[str | Content | Message]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def normalize_messages(
|
|
288
|
+
messages: AgentRunInputs | None = None,
|
|
289
|
+
) -> list[Message]:
|
|
290
|
+
"""Normalize message inputs to a list of Message objects.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
messages: The input messages in various supported formats. Can be:
|
|
294
|
+
- None (returns empty list)
|
|
295
|
+
- A string (converted to a user message)
|
|
296
|
+
- A Content object (wrapped in a user Message)
|
|
297
|
+
- A Message object
|
|
298
|
+
- A sequence containing any mix of the above
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
A list of Message objects.
|
|
302
|
+
"""
|
|
303
|
+
if messages is None:
|
|
304
|
+
return []
|
|
305
|
+
|
|
306
|
+
if isinstance(messages, str):
|
|
307
|
+
return [Message("user", [messages])]
|
|
308
|
+
|
|
309
|
+
if isinstance(messages, Content):
|
|
310
|
+
return [Message("user", [messages])]
|
|
311
|
+
|
|
312
|
+
if isinstance(messages, Message):
|
|
313
|
+
return [messages]
|
|
314
|
+
|
|
315
|
+
result: list[Message] = []
|
|
316
|
+
for msg in messages:
|
|
317
|
+
if isinstance(msg, (str, Content)):
|
|
318
|
+
result.append(Message("user", [msg]))
|
|
319
|
+
else:
|
|
320
|
+
result.append(msg)
|
|
321
|
+
return result
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def prepend_instructions_to_messages(
|
|
325
|
+
messages: list[Message],
|
|
326
|
+
instructions: str | Sequence[str] | None,
|
|
327
|
+
role: RoleLiteral | str = "system",
|
|
328
|
+
) -> list[Message]:
|
|
329
|
+
"""Prepend instructions to a list of messages with a specified role.
|
|
330
|
+
|
|
331
|
+
This is a helper method for chat clients that need to add instructions
|
|
332
|
+
from options as messages. Different providers support different roles for
|
|
333
|
+
instructions (e.g., OpenAI uses "system", some providers might use "user").
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
messages: The existing list of Message objects.
|
|
337
|
+
instructions: The instructions to prepend. Can be a single string or a sequence of strings.
|
|
338
|
+
role: The role to use for the instruction messages. Defaults to "system".
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
A new list with instruction messages prepended.
|
|
342
|
+
|
|
343
|
+
Examples:
|
|
344
|
+
.. code-block:: python
|
|
345
|
+
|
|
346
|
+
from chrys.kernel._types import prepend_instructions_to_messages, Message
|
|
347
|
+
|
|
348
|
+
messages = [Message("user", ["Hello"])]
|
|
349
|
+
instructions = "You are a helpful assistant"
|
|
350
|
+
|
|
351
|
+
# Prepend as system message (default)
|
|
352
|
+
messages_with_instructions = prepend_instructions_to_messages(messages, instructions)
|
|
353
|
+
|
|
354
|
+
# Or use a different role
|
|
355
|
+
messages_with_user_instructions = prepend_instructions_to_messages(messages, instructions, role="user")
|
|
356
|
+
"""
|
|
357
|
+
if instructions is None:
|
|
358
|
+
return messages
|
|
359
|
+
|
|
360
|
+
if isinstance(instructions, str):
|
|
361
|
+
instructions = [instructions]
|
|
362
|
+
|
|
363
|
+
# Skip instructions that are already present as leading messages with the
|
|
364
|
+
# same role and text. This prevents duplicate system messages when
|
|
365
|
+
# instructions are injected by multiple layers (e.g. Agent + chat client).
|
|
366
|
+
deduplicated: list[str] = []
|
|
367
|
+
for idx, instr in enumerate(instructions):
|
|
368
|
+
if idx < len(messages) and messages[idx].role == role and messages[idx].text == instr:
|
|
369
|
+
continue
|
|
370
|
+
deduplicated.append(instr)
|
|
371
|
+
|
|
372
|
+
if not deduplicated:
|
|
373
|
+
return messages
|
|
374
|
+
|
|
375
|
+
instruction_messages = [Message(role, [instr]) for instr in deduplicated]
|
|
376
|
+
return [*instruction_messages, *messages]
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# region ChatResponse
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _is_transport_heartbeat_update(update: ChatResponseUpdate | AgentResponseUpdate) -> bool:
|
|
383
|
+
"""Return whether *update* reports stream progress without response semantics."""
|
|
384
|
+
if isinstance(update, ChatResponseUpdate):
|
|
385
|
+
return update.is_transport_heartbeat
|
|
386
|
+
|
|
387
|
+
raw_update = update.raw_representation
|
|
388
|
+
return (
|
|
389
|
+
isinstance(raw_update, ChatResponseUpdate)
|
|
390
|
+
and raw_update.is_transport_heartbeat
|
|
391
|
+
and not update.contents
|
|
392
|
+
and update.role is None
|
|
393
|
+
and update.response_id is None
|
|
394
|
+
and update.message_id is None
|
|
395
|
+
and update.created_at is None
|
|
396
|
+
and update.finish_reason is None
|
|
397
|
+
and update.continuation_token is None
|
|
398
|
+
and not update.additional_properties
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def is_retry_boundary_update(update: ChatResponseUpdate | AgentResponseUpdate) -> bool:
|
|
403
|
+
"""Return whether *update* marks a fresh wire-attempt boundary."""
|
|
404
|
+
return (
|
|
405
|
+
not update.contents
|
|
406
|
+
and update.additional_properties is not None
|
|
407
|
+
and update.additional_properties.get(RETRY_BOUNDARY_UPDATE_KEY) is True
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _process_update(
|
|
412
|
+
response: ChatResponse | AgentResponse,
|
|
413
|
+
update: ChatResponseUpdate | AgentResponseUpdate,
|
|
414
|
+
seen_contents: dict[int, Content],
|
|
415
|
+
) -> None:
|
|
416
|
+
"""Processes a single update and modifies the response in place.
|
|
417
|
+
|
|
418
|
+
``seen_contents`` is one assembly pass's identity memo (each
|
|
419
|
+
``from_updates``-style caller seeds an empty dict and threads it through
|
|
420
|
+
every update): a content OBJECT the pass already incorporated is a
|
|
421
|
+
duplicate emission, not a new fragment, and is skipped. Without this, a
|
|
422
|
+
repeated function-call object adjacent to itself would SELF-MERGE —
|
|
423
|
+
concatenating its own argument string into unparseable JSON — turning a
|
|
424
|
+
valid call into an argument-parsing failure; the blocking path already
|
|
425
|
+
collapses identity duplicates within one response to a single copy at
|
|
426
|
+
landing, and assembly-scoped dedup gives streamed responses the same
|
|
427
|
+
semantics (per attempt, so a validation retry re-emitting an object a
|
|
428
|
+
REJECTED attempt streamed is unaffected — each assembly seeds its own
|
|
429
|
+
memo). The memo maps id -> content precisely to RETAIN each object for
|
|
430
|
+
the pass: generator-fed assembly holds no other reference to consumed
|
|
431
|
+
fragments, and a merged-away fragment released mid-pass would let
|
|
432
|
+
CPython hand a later fresh fragment the same id — silently dropping it.
|
|
433
|
+
Usage contents are exempt: they never land in messages and their
|
|
434
|
+
accumulation is value-based, not object-based.
|
|
435
|
+
"""
|
|
436
|
+
if _is_transport_heartbeat_update(update) or is_retry_boundary_update(update):
|
|
437
|
+
return
|
|
438
|
+
is_new_message = False
|
|
439
|
+
if (
|
|
440
|
+
not response.messages
|
|
441
|
+
or (
|
|
442
|
+
update.message_id
|
|
443
|
+
and response.messages[-1].message_id
|
|
444
|
+
and response.messages[-1].message_id != update.message_id
|
|
445
|
+
)
|
|
446
|
+
or (update.role and response.messages[-1].role != update.role)
|
|
447
|
+
):
|
|
448
|
+
is_new_message = True
|
|
449
|
+
|
|
450
|
+
message = Message("assistant", []) if is_new_message else response.messages[-1]
|
|
451
|
+
if isinstance(update, ChatResponseUpdate) and update._chrys_echo_content_stripped:
|
|
452
|
+
# Preserve which logical message — not merely which model call —
|
|
453
|
+
# lost echo content. The loop can then remove only a shell that
|
|
454
|
+
# remained empty after its accepted stream was assembled.
|
|
455
|
+
message._chrys_echo_content_stripped = True
|
|
456
|
+
skipped_duplicate_content = False
|
|
457
|
+
# Incorporate the update's properties into the message.
|
|
458
|
+
if update.author_name is not None:
|
|
459
|
+
message.author_name = update.author_name
|
|
460
|
+
if update.role is not None:
|
|
461
|
+
message.role = update.role
|
|
462
|
+
if update.message_id:
|
|
463
|
+
message.message_id = update.message_id
|
|
464
|
+
for content in update.contents:
|
|
465
|
+
# Fast path: get type attribute (most content will have it)
|
|
466
|
+
content_type = getattr(content, "type", None)
|
|
467
|
+
# Slow path: only check for dict if type is None
|
|
468
|
+
if content_type is None and isinstance(content, (dict, MutableMapping)):
|
|
469
|
+
try:
|
|
470
|
+
content = Content.from_dict(dict(content.items()))
|
|
471
|
+
content_type = content.type
|
|
472
|
+
except ContentError as exc:
|
|
473
|
+
logger.warning(f"Skipping unknown content type or invalid content: {exc}")
|
|
474
|
+
continue
|
|
475
|
+
if content_type != "usage":
|
|
476
|
+
if id(content) in seen_contents:
|
|
477
|
+
skipped_duplicate_content = True
|
|
478
|
+
continue
|
|
479
|
+
seen_contents[id(content)] = content
|
|
480
|
+
match content_type:
|
|
481
|
+
# mypy doesn't narrow type based on match/case, but we know these are FunctionCallContents
|
|
482
|
+
case "function_call" if message.contents and message.contents[-1].type == "function_call":
|
|
483
|
+
try:
|
|
484
|
+
message.contents[-1] += content # type: ignore[operator]
|
|
485
|
+
except AdditionItemMismatch, ContentError:
|
|
486
|
+
message.contents.append(content)
|
|
487
|
+
case "usage":
|
|
488
|
+
if response.usage_details is None:
|
|
489
|
+
response.usage_details = UsageDetails()
|
|
490
|
+
# mypy doesn't narrow type based on match/case, but we know this is UsageContent
|
|
491
|
+
response.usage_details = add_usage_details(response.usage_details, content.usage_details) # type: ignore[arg-type]
|
|
492
|
+
usage_details = content.usage_details or {}
|
|
493
|
+
response.latest_usage_details = normalize_stream_usage(
|
|
494
|
+
[response.latest_usage_details or {}, usage_details]
|
|
495
|
+
)
|
|
496
|
+
case _:
|
|
497
|
+
message.contents.append(content)
|
|
498
|
+
if is_new_message and not (skipped_duplicate_content and not message.contents):
|
|
499
|
+
# A duplicate object emitted under a fresh message boundary belongs
|
|
500
|
+
# to the message that already incorporated it. Do not leave behind an
|
|
501
|
+
# empty shell whose message_id/role makes response validation reject
|
|
502
|
+
# an otherwise valid streamed response. Preserve the existing shape
|
|
503
|
+
# for genuinely empty and usage-only updates.
|
|
504
|
+
response.messages.append(message)
|
|
505
|
+
# Incorporate the update's properties into the response.
|
|
506
|
+
if update.response_id:
|
|
507
|
+
response.response_id = update.response_id
|
|
508
|
+
if update.created_at is not None:
|
|
509
|
+
response.created_at = update.created_at
|
|
510
|
+
if update.additional_properties is not None:
|
|
511
|
+
response.additional_properties.update(update.additional_properties)
|
|
512
|
+
if response.raw_representation is None:
|
|
513
|
+
response.raw_representation = []
|
|
514
|
+
if not isinstance(response.raw_representation, list):
|
|
515
|
+
response.raw_representation = [response.raw_representation]
|
|
516
|
+
raw_representation_value = cast(Any, response.raw_representation)
|
|
517
|
+
raw_representation_list = cast(list[Any], raw_representation_value)
|
|
518
|
+
raw_representation_list.append(update.raw_representation)
|
|
519
|
+
if isinstance(response, ChatResponse) and isinstance(update, ChatResponseUpdate):
|
|
520
|
+
if update.conversation_id is not None:
|
|
521
|
+
response.conversation_id = update.conversation_id
|
|
522
|
+
if update.finish_reason is not None:
|
|
523
|
+
response.finish_reason = update.finish_reason
|
|
524
|
+
if update.model is not None:
|
|
525
|
+
response.model = update.model
|
|
526
|
+
if (
|
|
527
|
+
isinstance(response, AgentResponse)
|
|
528
|
+
and isinstance(update, AgentResponseUpdate)
|
|
529
|
+
and update.finish_reason is not None
|
|
530
|
+
):
|
|
531
|
+
response.finish_reason = update.finish_reason
|
|
532
|
+
response.continuation_token = update.continuation_token
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _coalesce_text_content(contents: list[Content], type_str: Literal["text", "text_reasoning"]) -> None:
|
|
536
|
+
"""Take any subsequence Text or TextReasoningContent items and coalesce them into a single item."""
|
|
537
|
+
if not contents:
|
|
538
|
+
return
|
|
539
|
+
coalesced_contents: list[Content] = []
|
|
540
|
+
first_new_content: Any | None = None
|
|
541
|
+
for content in contents:
|
|
542
|
+
if content.type == type_str:
|
|
543
|
+
if first_new_content is None:
|
|
544
|
+
first_new_content = deepcopy(content)
|
|
545
|
+
else:
|
|
546
|
+
try:
|
|
547
|
+
first_new_content += content
|
|
548
|
+
except AdditionItemMismatch:
|
|
549
|
+
# Different IDs means a new logical segment; flush the current one
|
|
550
|
+
coalesced_contents.append(first_new_content)
|
|
551
|
+
first_new_content = deepcopy(content)
|
|
552
|
+
else:
|
|
553
|
+
# skip this content, it is not of the right type
|
|
554
|
+
# so write the existing one to the list and start a new one,
|
|
555
|
+
# once the right type is found again
|
|
556
|
+
if first_new_content:
|
|
557
|
+
coalesced_contents.append(first_new_content)
|
|
558
|
+
first_new_content = None
|
|
559
|
+
# but keep the other content in the new list
|
|
560
|
+
coalesced_contents.append(content)
|
|
561
|
+
if first_new_content:
|
|
562
|
+
coalesced_contents.append(first_new_content)
|
|
563
|
+
contents.clear()
|
|
564
|
+
contents.extend(coalesced_contents)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _content_items_text(items: Any) -> str | None:
|
|
568
|
+
"""Return concatenated text when a content item list only contains text."""
|
|
569
|
+
if not isinstance(items, list):
|
|
570
|
+
return None
|
|
571
|
+
text_parts: list[str] = []
|
|
572
|
+
content_items = cast(list[object], items)
|
|
573
|
+
for item in content_items:
|
|
574
|
+
if not isinstance(item, Content) or item.type != "text":
|
|
575
|
+
return None
|
|
576
|
+
text_parts.append(item.text or "")
|
|
577
|
+
return "".join(text_parts)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _merge_content_item_lists(existing: Any, incoming: Any) -> Any:
|
|
581
|
+
"""Merge streamed nested content lists, replacing deltas with a later full value when present."""
|
|
582
|
+
if incoming is None:
|
|
583
|
+
return existing
|
|
584
|
+
if existing is None:
|
|
585
|
+
return deepcopy(incoming)
|
|
586
|
+
|
|
587
|
+
existing_text = _content_items_text(existing)
|
|
588
|
+
incoming_text = _content_items_text(incoming)
|
|
589
|
+
if existing_text is not None and incoming_text is not None:
|
|
590
|
+
if incoming_text.startswith(existing_text):
|
|
591
|
+
return deepcopy(incoming)
|
|
592
|
+
if existing_text.startswith(incoming_text):
|
|
593
|
+
return existing
|
|
594
|
+
|
|
595
|
+
existing_items = cast(list[Content], existing)
|
|
596
|
+
merged = deepcopy(existing_items[0])
|
|
597
|
+
merged.text = existing_text + incoming_text
|
|
598
|
+
return [merged]
|
|
599
|
+
|
|
600
|
+
if isinstance(existing, list) and isinstance(incoming, list):
|
|
601
|
+
existing_list = cast(list[object], existing)
|
|
602
|
+
incoming_list = cast(list[object], incoming)
|
|
603
|
+
return [*existing_list, *deepcopy(incoming_list)]
|
|
604
|
+
return deepcopy(incoming)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _merge_code_interpreter_content(existing: Content, incoming: Content) -> None:
|
|
608
|
+
"""Merge two code interpreter content items for the same logical call."""
|
|
609
|
+
existing.inputs = _merge_content_item_lists(existing.inputs, incoming.inputs)
|
|
610
|
+
existing.outputs = _merge_content_item_lists(existing.outputs, incoming.outputs)
|
|
611
|
+
existing.annotations = _combine_annotations(existing.annotations, incoming.annotations)
|
|
612
|
+
existing.additional_properties = {**existing.additional_properties, **incoming.additional_properties}
|
|
613
|
+
existing.raw_representation = _combine_raw_representations(existing.raw_representation, incoming.raw_representation)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _code_interpreter_key(content: Content) -> tuple[str, str] | None:
|
|
617
|
+
"""Return the aggregation key for code interpreter call/result content."""
|
|
618
|
+
if content.type not in {"code_interpreter_tool_call", "code_interpreter_tool_result"}:
|
|
619
|
+
return None
|
|
620
|
+
call_id = content.call_id or content.additional_properties.get("item_id")
|
|
621
|
+
if not isinstance(call_id, str) or not call_id:
|
|
622
|
+
return None
|
|
623
|
+
return content.type, call_id
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _coalesce_code_interpreter_content(contents: list[Content]) -> None:
|
|
627
|
+
"""Coalesce streaming code interpreter chunks by call id."""
|
|
628
|
+
if not contents:
|
|
629
|
+
return
|
|
630
|
+
|
|
631
|
+
coalesced_contents: list[Content] = []
|
|
632
|
+
seen: dict[tuple[str, str], Content] = {}
|
|
633
|
+
for content in contents:
|
|
634
|
+
key = _code_interpreter_key(content)
|
|
635
|
+
if key is None:
|
|
636
|
+
coalesced_contents.append(content)
|
|
637
|
+
continue
|
|
638
|
+
|
|
639
|
+
existing = seen.get(key)
|
|
640
|
+
if existing is None:
|
|
641
|
+
copied = deepcopy(content)
|
|
642
|
+
seen[key] = copied
|
|
643
|
+
coalesced_contents.append(copied)
|
|
644
|
+
continue
|
|
645
|
+
|
|
646
|
+
_merge_code_interpreter_content(existing, content)
|
|
647
|
+
|
|
648
|
+
contents.clear()
|
|
649
|
+
contents.extend(coalesced_contents)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _finalize_response(response: ChatResponse | AgentResponse) -> None:
|
|
653
|
+
"""Finalizes the response by performing any necessary post-processing."""
|
|
654
|
+
# A single response stream may carry cumulative usage snapshots. Keep
|
|
655
|
+
# only the latest per-key values; a surrounding tool loop explicitly
|
|
656
|
+
# restores the aggregate across its distinct model calls afterwards.
|
|
657
|
+
if response.latest_usage_details is not None:
|
|
658
|
+
response.usage_details = response.latest_usage_details
|
|
659
|
+
for msg in response.messages:
|
|
660
|
+
_coalesce_text_content(msg.contents, "text")
|
|
661
|
+
_coalesce_text_content(msg.contents, "text_reasoning")
|
|
662
|
+
_coalesce_code_interpreter_content(msg.contents)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
# region ContinuationToken
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
class ContinuationToken(TypedDict):
|
|
669
|
+
"""Opaque token for resuming long-running agent operations.
|
|
670
|
+
|
|
671
|
+
A JSON-serializable dict used to poll for completion or resume a
|
|
672
|
+
streaming response. Presence on a response indicates the operation
|
|
673
|
+
is still in progress; ``None`` means the operation is complete.
|
|
674
|
+
|
|
675
|
+
Each provider subclasses this with its own fields; consumers should
|
|
676
|
+
treat the token as opaque and simply pass it back to the same agent.
|
|
677
|
+
|
|
678
|
+
Examples:
|
|
679
|
+
.. code-block:: python
|
|
680
|
+
|
|
681
|
+
import json
|
|
682
|
+
|
|
683
|
+
# Persist token across restarts
|
|
684
|
+
token_json = json.dumps(response.continuation_token)
|
|
685
|
+
|
|
686
|
+
# Restore and resume
|
|
687
|
+
token = json.loads(token_json)
|
|
688
|
+
response = await agent.run(
|
|
689
|
+
session=session,
|
|
690
|
+
options={"continuation_token": token},
|
|
691
|
+
)
|
|
692
|
+
"""
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
# endregion
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _parse_structured_response_value(text: str, response_format: Any | None) -> Any | None:
|
|
699
|
+
if response_format is None:
|
|
700
|
+
return None
|
|
701
|
+
if not text:
|
|
702
|
+
return None
|
|
703
|
+
if isinstance(response_format, type) and issubclass(response_format, BaseModel):
|
|
704
|
+
return response_format.model_validate_json(text)
|
|
705
|
+
if isinstance(response_format, Mapping):
|
|
706
|
+
try:
|
|
707
|
+
return json.loads(text)
|
|
708
|
+
except json.JSONDecodeError as exc:
|
|
709
|
+
raise ValueError(f"Response text is not valid JSON: {exc}") from exc
|
|
710
|
+
logger.warning(
|
|
711
|
+
"Unable to parse structured response value, use either a Pydantic model or a dict defining the schema, "
|
|
712
|
+
"received response_format type: %s",
|
|
713
|
+
type(response_format), # type: ignore[reportUnknownArgumentType]
|
|
714
|
+
)
|
|
715
|
+
return None
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def _last_non_empty_assistant_message_text(messages: Sequence[Message]) -> str:
|
|
719
|
+
"""Return structured-output text from the final substantive assistant message."""
|
|
720
|
+
for message in reversed(messages):
|
|
721
|
+
if message.role != "assistant":
|
|
722
|
+
continue
|
|
723
|
+
text = "".join((content.text or "") for content in message.contents if content.type == "text")
|
|
724
|
+
if text.strip():
|
|
725
|
+
return text
|
|
726
|
+
return ""
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
_T = TypeVar("_T")
|
|
730
|
+
ChatResponseT = TypeVar("ChatResponseT", bound="ChatResponse")
|
|
731
|
+
ToolModeT = TypeVar("ToolModeT", bound="ToolMode")
|
|
732
|
+
AgentResponseT = TypeVar("AgentResponseT", bound="AgentResponse")
|
|
733
|
+
ResponseModelT = TypeVar("ResponseModelT", bound=BaseModel | None, default=None, covariant=True)
|
|
734
|
+
ResponseModelBoundT = TypeVar("ResponseModelBoundT", bound=BaseModel)
|
|
735
|
+
StructuredResponseFormat = type[BaseModel] | Mapping[str, Any] | None
|
|
736
|
+
|
|
737
|
+
CreatedAtT = str # Use a datetimeoffset type? Or a more specific type like datetime.datetime?
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
class ChatResponse(SerializationMixin, Generic[ResponseModelT]):
|
|
741
|
+
"""Represents the response to a chat request.
|
|
742
|
+
|
|
743
|
+
Attributes:
|
|
744
|
+
messages: The list of chat messages in the response.
|
|
745
|
+
response_id: The ID of the chat response.
|
|
746
|
+
conversation_id: An identifier for the state of the conversation.
|
|
747
|
+
model: The model used in the creation of the chat response.
|
|
748
|
+
created_at: A timestamp for the chat response.
|
|
749
|
+
finish_reason: The reason for the chat response.
|
|
750
|
+
usage_details: Aggregate usage across all model calls represented by the response.
|
|
751
|
+
latest_usage_details: Usage for the most recent model call only.
|
|
752
|
+
structured_output: The structured output of the chat response, if applicable.
|
|
753
|
+
additional_properties: Any additional properties associated with the chat response.
|
|
754
|
+
raw_representation: The raw representation of the chat response from an underlying implementation.
|
|
755
|
+
|
|
756
|
+
Note:
|
|
757
|
+
The `author_name` attribute is available on the `Message` objects inside `messages`,
|
|
758
|
+
not on the `ChatResponse` itself. Use `response.messages[0].author_name` to access
|
|
759
|
+
the author name of individual messages.
|
|
760
|
+
|
|
761
|
+
Examples:
|
|
762
|
+
.. code-block:: python
|
|
763
|
+
|
|
764
|
+
from chrys.kernel._types import ChatResponse, Message
|
|
765
|
+
|
|
766
|
+
# Create a response with messages
|
|
767
|
+
msg = Message("assistant", ["The weather is sunny."])
|
|
768
|
+
response = ChatResponse(
|
|
769
|
+
messages=[msg],
|
|
770
|
+
finish_reason="stop",
|
|
771
|
+
model="gpt-4",
|
|
772
|
+
)
|
|
773
|
+
print(response.text) # "The weather is sunny."
|
|
774
|
+
|
|
775
|
+
# Combine streaming updates
|
|
776
|
+
updates = [...] # List of ChatResponseUpdate objects
|
|
777
|
+
response = ChatResponse.from_updates(updates)
|
|
778
|
+
|
|
779
|
+
# Serialization - to_dict and from_dict
|
|
780
|
+
response_dict = response.to_dict()
|
|
781
|
+
# {'type': 'chat_response', 'messages': [...], 'model': 'gpt-4', 'finish_reason': 'stop'}
|
|
782
|
+
restored_response = ChatResponse.from_dict(response_dict)
|
|
783
|
+
print(restored_response.model) # "gpt-4"
|
|
784
|
+
|
|
785
|
+
# Serialization - to_json and from_json
|
|
786
|
+
response_json = response.to_json()
|
|
787
|
+
# '{"type": "chat_response", "messages": [...], "model": "gpt-4", ...}'
|
|
788
|
+
restored_from_json = ChatResponse.from_json(response_json)
|
|
789
|
+
print(restored_from_json.text) # "The weather is sunny."
|
|
790
|
+
"""
|
|
791
|
+
|
|
792
|
+
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"raw_representation", "additional_properties"}
|
|
793
|
+
_INTERNAL_CONVERSATION_ID_KEY: ClassVar[str] = "_chrys_internal_conversation_id"
|
|
794
|
+
|
|
795
|
+
def __init__(
|
|
796
|
+
self,
|
|
797
|
+
*,
|
|
798
|
+
messages: Message | Sequence[Message] | None = None,
|
|
799
|
+
response_id: str | None = None,
|
|
800
|
+
conversation_id: str | None = None,
|
|
801
|
+
model: str | None = None,
|
|
802
|
+
created_at: CreatedAtT | None = None,
|
|
803
|
+
finish_reason: FinishReasonLiteral | FinishReason | None = None,
|
|
804
|
+
usage_details: UsageDetails | None = None,
|
|
805
|
+
latest_usage_details: UsageDetails | None = _UNSET_USAGE,
|
|
806
|
+
value: ResponseModelT | None = None,
|
|
807
|
+
response_format: StructuredResponseFormat = None,
|
|
808
|
+
continuation_token: ContinuationToken | None = None,
|
|
809
|
+
additional_properties: dict[str, Any] | None = None,
|
|
810
|
+
raw_representation: Any | None = None,
|
|
811
|
+
) -> None:
|
|
812
|
+
"""Initializes a ChatResponse with the provided parameters.
|
|
813
|
+
|
|
814
|
+
Keyword Args:
|
|
815
|
+
messages: A single Message or sequence of Message objects to include in the response.
|
|
816
|
+
response_id: Optional ID of the chat response.
|
|
817
|
+
conversation_id: Optional identifier for the state of the conversation.
|
|
818
|
+
model: Optional model used in the creation of the chat response.
|
|
819
|
+
created_at: Optional timestamp for when the response was created.
|
|
820
|
+
finish_reason: Optional reason for the chat response (e.g., "stop", "length", "tool_calls").
|
|
821
|
+
usage_details: Optional aggregate usage for the chat response.
|
|
822
|
+
latest_usage_details: Optional usage for the most recent model call.
|
|
823
|
+
When omitted, defaults to ``usage_details`` (single-call response).
|
|
824
|
+
Pass ``None`` explicitly when the final call's usage is unknown;
|
|
825
|
+
it is preserved rather than falling back to the aggregate.
|
|
826
|
+
value: Optional value of the structured output.
|
|
827
|
+
response_format: Optional response format for the chat response.
|
|
828
|
+
continuation_token: Optional token for resuming a long-running background operation.
|
|
829
|
+
When present, indicates the operation is still in progress.
|
|
830
|
+
additional_properties: Optional additional properties associated with the chat response.
|
|
831
|
+
raw_representation: Optional raw representation of the chat response from an underlying implementation.
|
|
832
|
+
"""
|
|
833
|
+
if messages is None:
|
|
834
|
+
self.messages: list[Message] = []
|
|
835
|
+
elif isinstance(messages, Message):
|
|
836
|
+
self.messages = [messages]
|
|
837
|
+
else:
|
|
838
|
+
# Handle both Message objects and dicts (for from_dict support)
|
|
839
|
+
processed_messages: list[Message] = []
|
|
840
|
+
for msg in messages:
|
|
841
|
+
if isinstance(msg, Message):
|
|
842
|
+
processed_messages.append(msg)
|
|
843
|
+
elif isinstance(msg, dict):
|
|
844
|
+
processed_messages.append(Message.from_dict(msg))
|
|
845
|
+
else:
|
|
846
|
+
processed_messages.append(msg)
|
|
847
|
+
self.messages = processed_messages
|
|
848
|
+
self.response_id = response_id
|
|
849
|
+
self.conversation_id = conversation_id
|
|
850
|
+
self.model = model
|
|
851
|
+
self.created_at = created_at
|
|
852
|
+
self.finish_reason = finish_reason
|
|
853
|
+
self.usage_details = usage_details
|
|
854
|
+
# ``usage_details`` may aggregate multiple tool-loop iterations for
|
|
855
|
+
# billing; this value represents the final request's context occupancy.
|
|
856
|
+
# Public attribute so serialization round-trips preserve both views.
|
|
857
|
+
self.latest_usage_details: UsageDetails | None = (
|
|
858
|
+
usage_details if latest_usage_details is _UNSET_USAGE else latest_usage_details
|
|
859
|
+
)
|
|
860
|
+
self._value: ResponseModelT | None = value
|
|
861
|
+
self._response_format: StructuredResponseFormat = response_format
|
|
862
|
+
self._value_parsed: bool = value is not None
|
|
863
|
+
self.additional_properties = (
|
|
864
|
+
_restore_compaction_annotation_in_additional_properties(additional_properties) or {}
|
|
865
|
+
)
|
|
866
|
+
self.continuation_token = continuation_token
|
|
867
|
+
self.raw_representation: Any | list[Any] | None = raw_representation
|
|
868
|
+
# Runtime-only marker set by the tool loop's exhaustion tail when it
|
|
869
|
+
# stripped content that a service-stored conversation still holds:
|
|
870
|
+
# continuation handles derived from this response must not be
|
|
871
|
+
# propagated or restored. Private attributes are excluded from
|
|
872
|
+
# serialization.
|
|
873
|
+
self._chrys_service_state_invalidated = False
|
|
874
|
+
|
|
875
|
+
def to_dict(self, *, exclude: set[str] | None = None, exclude_none: bool = True) -> dict[str, Any]:
|
|
876
|
+
"""Serialize, preserving an explicitly-unavailable latest usage.
|
|
877
|
+
|
|
878
|
+
``exclude_none`` would drop ``latest_usage_details=None`` and the
|
|
879
|
+
constructor default would then substitute the billing aggregate on
|
|
880
|
+
deserialization. ``None`` is a meaningful value here (final call's
|
|
881
|
+
usage unknown), so it is kept as an explicit ``null`` whenever an
|
|
882
|
+
aggregate exists to be wrongly inherited.
|
|
883
|
+
"""
|
|
884
|
+
result = super().to_dict(exclude=exclude, exclude_none=exclude_none)
|
|
885
|
+
if (
|
|
886
|
+
exclude_none
|
|
887
|
+
and self.latest_usage_details is None
|
|
888
|
+
and "latest_usage_details" not in (exclude or ())
|
|
889
|
+
and "usage_details" in result
|
|
890
|
+
):
|
|
891
|
+
result["latest_usage_details"] = None
|
|
892
|
+
return result
|
|
893
|
+
|
|
894
|
+
def mark_internal_conversation_id(self) -> None:
|
|
895
|
+
"""Mark the current conversation_id as internal control-flow state."""
|
|
896
|
+
self.additional_properties[self._INTERNAL_CONVERSATION_ID_KEY] = True
|
|
897
|
+
|
|
898
|
+
def clear_internal_conversation_id(self) -> None:
|
|
899
|
+
"""Remove the internal conversation-id marker."""
|
|
900
|
+
self.additional_properties.pop(self._INTERNAL_CONVERSATION_ID_KEY, None)
|
|
901
|
+
|
|
902
|
+
def has_internal_conversation_id(self) -> bool:
|
|
903
|
+
"""Return whether conversation_id is internal control-flow state."""
|
|
904
|
+
return bool(self.additional_properties.get(self._INTERNAL_CONVERSATION_ID_KEY, False))
|
|
905
|
+
|
|
906
|
+
@overload
|
|
907
|
+
@classmethod
|
|
908
|
+
def from_updates(
|
|
909
|
+
cls: type[ChatResponse[Any]],
|
|
910
|
+
updates: Sequence[ChatResponseUpdate],
|
|
911
|
+
*,
|
|
912
|
+
output_format_type: type[ResponseModelBoundT],
|
|
913
|
+
) -> ChatResponse[ResponseModelBoundT]: ...
|
|
914
|
+
|
|
915
|
+
@overload
|
|
916
|
+
@classmethod
|
|
917
|
+
def from_updates(
|
|
918
|
+
cls: type[ChatResponse[Any]],
|
|
919
|
+
updates: Sequence[ChatResponseUpdate],
|
|
920
|
+
*,
|
|
921
|
+
output_format_type: Mapping[str, Any],
|
|
922
|
+
) -> ChatResponse[Any]: ...
|
|
923
|
+
|
|
924
|
+
@overload
|
|
925
|
+
@classmethod
|
|
926
|
+
def from_updates(
|
|
927
|
+
cls: type[ChatResponse[Any]],
|
|
928
|
+
updates: Sequence[ChatResponseUpdate],
|
|
929
|
+
*,
|
|
930
|
+
output_format_type: None = None,
|
|
931
|
+
) -> ChatResponse[Any]: ...
|
|
932
|
+
|
|
933
|
+
@classmethod
|
|
934
|
+
def from_updates(
|
|
935
|
+
cls: type[ChatResponseT],
|
|
936
|
+
updates: Sequence[ChatResponseUpdate],
|
|
937
|
+
*,
|
|
938
|
+
output_format_type: StructuredResponseFormat = None,
|
|
939
|
+
) -> ChatResponseT:
|
|
940
|
+
"""Joins multiple updates into a single ChatResponse.
|
|
941
|
+
|
|
942
|
+
Example:
|
|
943
|
+
.. code-block:: python
|
|
944
|
+
|
|
945
|
+
from chrys.kernel._types import ChatResponse, ChatResponseUpdate
|
|
946
|
+
|
|
947
|
+
# Create some response updates
|
|
948
|
+
updates = [
|
|
949
|
+
ChatResponseUpdate(contents=[Content.from_text(text="Hello")], role="assistant"),
|
|
950
|
+
ChatResponseUpdate(contents=[Content.from_text(text=" How can I help you?")]),
|
|
951
|
+
]
|
|
952
|
+
|
|
953
|
+
# Combine updates into a single ChatResponse
|
|
954
|
+
response = ChatResponse.from_updates(updates)
|
|
955
|
+
print(response.text) # "Hello How can I help you?"
|
|
956
|
+
|
|
957
|
+
Args:
|
|
958
|
+
updates: A sequence of ChatResponseUpdate objects to combine.
|
|
959
|
+
|
|
960
|
+
Keyword Args:
|
|
961
|
+
output_format_type: Optional Pydantic model type or JSON schema mapping used to parse the
|
|
962
|
+
response text into structured data.
|
|
963
|
+
"""
|
|
964
|
+
msg = cls(messages=[], response_format=output_format_type)
|
|
965
|
+
seen_contents: dict[int, Content] = {}
|
|
966
|
+
for update in updates:
|
|
967
|
+
_process_update(msg, update, seen_contents)
|
|
968
|
+
_finalize_response(msg)
|
|
969
|
+
return msg
|
|
970
|
+
|
|
971
|
+
@overload
|
|
972
|
+
@classmethod
|
|
973
|
+
async def from_update_generator(
|
|
974
|
+
cls: type[ChatResponse[Any]],
|
|
975
|
+
updates: AsyncIterable[ChatResponseUpdate],
|
|
976
|
+
*,
|
|
977
|
+
output_format_type: type[ResponseModelBoundT],
|
|
978
|
+
) -> ChatResponse[ResponseModelBoundT]: ...
|
|
979
|
+
|
|
980
|
+
@overload
|
|
981
|
+
@classmethod
|
|
982
|
+
async def from_update_generator(
|
|
983
|
+
cls: type[ChatResponse[Any]],
|
|
984
|
+
updates: AsyncIterable[ChatResponseUpdate],
|
|
985
|
+
*,
|
|
986
|
+
output_format_type: Mapping[str, Any],
|
|
987
|
+
) -> ChatResponse[Any]: ...
|
|
988
|
+
|
|
989
|
+
@overload
|
|
990
|
+
@classmethod
|
|
991
|
+
async def from_update_generator(
|
|
992
|
+
cls: type[ChatResponse[Any]],
|
|
993
|
+
updates: AsyncIterable[ChatResponseUpdate],
|
|
994
|
+
*,
|
|
995
|
+
output_format_type: None = None,
|
|
996
|
+
) -> ChatResponse[Any]: ...
|
|
997
|
+
|
|
998
|
+
@classmethod
|
|
999
|
+
async def from_update_generator(
|
|
1000
|
+
cls: type[ChatResponseT],
|
|
1001
|
+
updates: AsyncIterable[ChatResponseUpdate],
|
|
1002
|
+
*,
|
|
1003
|
+
output_format_type: StructuredResponseFormat = None,
|
|
1004
|
+
) -> ChatResponseT:
|
|
1005
|
+
"""Joins multiple updates into a single ChatResponse.
|
|
1006
|
+
|
|
1007
|
+
Example:
|
|
1008
|
+
.. code-block:: python
|
|
1009
|
+
|
|
1010
|
+
from chrys.kernel._types import ChatResponse
|
|
1011
|
+
|
|
1012
|
+
response = await ChatResponse.from_update_generator(
|
|
1013
|
+
updates
|
|
1014
|
+
)
|
|
1015
|
+
print(response.text)
|
|
1016
|
+
|
|
1017
|
+
Args:
|
|
1018
|
+
updates: An async iterable of ChatResponseUpdate objects to combine.
|
|
1019
|
+
|
|
1020
|
+
Keyword Args:
|
|
1021
|
+
output_format_type: Optional Pydantic model type or JSON schema mapping used to parse the
|
|
1022
|
+
response text into structured data.
|
|
1023
|
+
"""
|
|
1024
|
+
msg = cls(messages=[], response_format=output_format_type)
|
|
1025
|
+
seen_contents: dict[int, Content] = {}
|
|
1026
|
+
async for update in updates:
|
|
1027
|
+
_process_update(msg, update, seen_contents)
|
|
1028
|
+
_finalize_response(msg)
|
|
1029
|
+
return msg
|
|
1030
|
+
|
|
1031
|
+
@property
|
|
1032
|
+
def text(self) -> str:
|
|
1033
|
+
"""Returns the concatenated text of all messages in the response."""
|
|
1034
|
+
return self.raw_text.strip()
|
|
1035
|
+
|
|
1036
|
+
@property
|
|
1037
|
+
def raw_text(self) -> str:
|
|
1038
|
+
"""The concatenated message text without the outer-whitespace strip of ``text``.
|
|
1039
|
+
|
|
1040
|
+
Format-sensitive consumers need the provider text verbatim: the
|
|
1041
|
+
LAST_WORDS structured-note validator treats leading indentation as
|
|
1042
|
+
CommonMark-meaningful, so a stripped view would silently legitimize
|
|
1043
|
+
a code-block-indented heading.
|
|
1044
|
+
"""
|
|
1045
|
+
return "\n".join(message.text for message in self.messages if isinstance(message, Message))
|
|
1046
|
+
|
|
1047
|
+
@property
|
|
1048
|
+
def value(self) -> ResponseModelT | None:
|
|
1049
|
+
"""Get the parsed structured output value.
|
|
1050
|
+
|
|
1051
|
+
If a response_format was provided and parsing hasn't been attempted yet,
|
|
1052
|
+
this parses the final non-empty assistant message into the specified type.
|
|
1053
|
+
|
|
1054
|
+
Raises:
|
|
1055
|
+
ValidationError: If the response text doesn't match the expected schema.
|
|
1056
|
+
ValueError: If the response text is not valid JSON for a non-Pydantic structured format.
|
|
1057
|
+
"""
|
|
1058
|
+
if self._value_parsed:
|
|
1059
|
+
return self._value
|
|
1060
|
+
if self._response_format is not None:
|
|
1061
|
+
self._value = cast(
|
|
1062
|
+
ResponseModelT,
|
|
1063
|
+
_parse_structured_response_value(
|
|
1064
|
+
_last_non_empty_assistant_message_text(self.messages),
|
|
1065
|
+
self._response_format,
|
|
1066
|
+
),
|
|
1067
|
+
)
|
|
1068
|
+
self._value_parsed = True
|
|
1069
|
+
return self._value
|
|
1070
|
+
|
|
1071
|
+
def __str__(self) -> str:
|
|
1072
|
+
return self.text
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
# region ChatResponseUpdate
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
class ChatResponseUpdate(SerializationMixin):
|
|
1079
|
+
"""Represents a single streaming response chunk from a `ChatClient`.
|
|
1080
|
+
|
|
1081
|
+
Attributes:
|
|
1082
|
+
contents: The chat response update content items.
|
|
1083
|
+
role: The role of the author of the response update.
|
|
1084
|
+
author_name: The name of the author of the response update. This is primarily used in
|
|
1085
|
+
multi-agent scenarios to identify which agent or participant generated the response.
|
|
1086
|
+
When updates are combined into a `ChatResponse`, the `author_name` is propagated
|
|
1087
|
+
to the resulting `Message` objects.
|
|
1088
|
+
response_id: The ID of the response of which this update is a part.
|
|
1089
|
+
message_id: The ID of the message of which this update is a part.
|
|
1090
|
+
conversation_id: An identifier for the state of the conversation of which this update is a part.
|
|
1091
|
+
model: The model associated with this response update.
|
|
1092
|
+
created_at: A timestamp for the chat response update.
|
|
1093
|
+
finish_reason: The finish reason for the operation.
|
|
1094
|
+
additional_properties: Any additional properties associated with the chat response update.
|
|
1095
|
+
raw_representation: The raw representation of the chat response update from an underlying implementation.
|
|
1096
|
+
|
|
1097
|
+
Examples:
|
|
1098
|
+
.. code-block:: python
|
|
1099
|
+
|
|
1100
|
+
from chrys.kernel._content import Content
|
|
1101
|
+
from chrys.kernel._types import ChatResponseUpdate
|
|
1102
|
+
|
|
1103
|
+
# Create a response update with text content
|
|
1104
|
+
update = ChatResponseUpdate(
|
|
1105
|
+
contents=[Content.from_text(text="Hello")],
|
|
1106
|
+
role="assistant",
|
|
1107
|
+
message_id="msg_123",
|
|
1108
|
+
)
|
|
1109
|
+
print(update.text) # "Hello"
|
|
1110
|
+
|
|
1111
|
+
# Serialization - to_dict and from_dict
|
|
1112
|
+
update_dict = update.to_dict()
|
|
1113
|
+
# {'type': 'chat_response_update', 'contents': [{'type': 'text', 'text': 'Hello'}],
|
|
1114
|
+
# 'role': 'assistant', 'message_id': 'msg_123'}
|
|
1115
|
+
restored_update = ChatResponseUpdate.from_dict(update_dict)
|
|
1116
|
+
print(restored_update.text) # "Hello"
|
|
1117
|
+
|
|
1118
|
+
# Serialization - to_json and from_json
|
|
1119
|
+
update_json = update.to_json()
|
|
1120
|
+
# '{"type": "chat_response_update", "contents": [{"type": "text", "text": "Hello"}], ...}'
|
|
1121
|
+
restored_from_json = ChatResponseUpdate.from_json(update_json)
|
|
1122
|
+
print(restored_from_json.message_id) # "msg_123"
|
|
1123
|
+
|
|
1124
|
+
"""
|
|
1125
|
+
|
|
1126
|
+
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"raw_representation"}
|
|
1127
|
+
|
|
1128
|
+
def __init__(
|
|
1129
|
+
self,
|
|
1130
|
+
*,
|
|
1131
|
+
contents: Sequence[Content] | None = None,
|
|
1132
|
+
role: RoleLiteral | Role | None = None,
|
|
1133
|
+
author_name: str | None = None,
|
|
1134
|
+
response_id: str | None = None,
|
|
1135
|
+
message_id: str | None = None,
|
|
1136
|
+
conversation_id: str | None = None,
|
|
1137
|
+
model: str | None = None,
|
|
1138
|
+
created_at: CreatedAtT | None = None,
|
|
1139
|
+
finish_reason: FinishReasonLiteral | FinishReason | None = None,
|
|
1140
|
+
continuation_token: ContinuationToken | None = None,
|
|
1141
|
+
additional_properties: dict[str, Any] | None = None,
|
|
1142
|
+
raw_representation: Any | None = None,
|
|
1143
|
+
) -> None:
|
|
1144
|
+
"""Initializes a ChatResponseUpdate with the provided parameters.
|
|
1145
|
+
|
|
1146
|
+
Keyword Args:
|
|
1147
|
+
contents: Optional list of Content items to include in the update.
|
|
1148
|
+
role: Optional role of the author of the response update (e.g., "user", "assistant").
|
|
1149
|
+
author_name: Optional name of the author of the response update.
|
|
1150
|
+
response_id: Optional ID of the response of which this update is a part.
|
|
1151
|
+
message_id: Optional ID of the message of which this update is a part.
|
|
1152
|
+
conversation_id: Optional identifier for the state of the conversation of which this update is a part
|
|
1153
|
+
model: Optional model associated with this response update.
|
|
1154
|
+
created_at: Optional timestamp for the chat response update.
|
|
1155
|
+
finish_reason: Optional finish reason for the operation.
|
|
1156
|
+
continuation_token: Optional token for resuming a long-running background operation.
|
|
1157
|
+
When present, indicates the operation is still in progress.
|
|
1158
|
+
additional_properties: Optional additional properties associated with the chat response update.
|
|
1159
|
+
raw_representation: Optional raw representation of the chat response update
|
|
1160
|
+
from an underlying implementation.
|
|
1161
|
+
|
|
1162
|
+
"""
|
|
1163
|
+
# Handle contents - support dict conversion for from_dict
|
|
1164
|
+
if contents is None:
|
|
1165
|
+
self.contents: list[Content] = []
|
|
1166
|
+
else:
|
|
1167
|
+
processed_contents: list[Content] = []
|
|
1168
|
+
for c in contents:
|
|
1169
|
+
if isinstance(c, Content):
|
|
1170
|
+
processed_contents.append(c)
|
|
1171
|
+
elif isinstance(c, dict):
|
|
1172
|
+
processed_contents.append(Content.from_dict(c))
|
|
1173
|
+
else:
|
|
1174
|
+
processed_contents.append(c)
|
|
1175
|
+
self.contents = processed_contents
|
|
1176
|
+
|
|
1177
|
+
self.role = role
|
|
1178
|
+
self.author_name = author_name
|
|
1179
|
+
self.response_id = response_id
|
|
1180
|
+
self.message_id = message_id
|
|
1181
|
+
self.conversation_id = conversation_id
|
|
1182
|
+
self.model = model
|
|
1183
|
+
self.created_at = created_at
|
|
1184
|
+
self.finish_reason = finish_reason
|
|
1185
|
+
self.continuation_token = continuation_token
|
|
1186
|
+
self.additional_properties = _restore_compaction_annotation_in_additional_properties(
|
|
1187
|
+
additional_properties,
|
|
1188
|
+
allow_none=True,
|
|
1189
|
+
)
|
|
1190
|
+
self.raw_representation = raw_representation
|
|
1191
|
+
# Runtime-only provenance copied through stream proxies and folded
|
|
1192
|
+
# into the corresponding Message by _process_update.
|
|
1193
|
+
self._chrys_echo_content_stripped = False
|
|
1194
|
+
|
|
1195
|
+
@classmethod
|
|
1196
|
+
def transport_heartbeat(cls) -> ChatResponseUpdate:
|
|
1197
|
+
"""Create an opaque progress update carrying no provider response data."""
|
|
1198
|
+
return cls(contents=[], raw_representation=_STREAM_HEARTBEAT_MARKER)
|
|
1199
|
+
|
|
1200
|
+
@classmethod
|
|
1201
|
+
def retry_boundary(cls) -> ChatResponseUpdate:
|
|
1202
|
+
"""Create an in-band marker for a fresh logical-call attempt."""
|
|
1203
|
+
return cls(contents=[], additional_properties={RETRY_BOUNDARY_UPDATE_KEY: True})
|
|
1204
|
+
|
|
1205
|
+
@property
|
|
1206
|
+
def is_transport_heartbeat(self) -> bool:
|
|
1207
|
+
"""Return whether this is the explicit opaque transport heartbeat."""
|
|
1208
|
+
return (
|
|
1209
|
+
self.raw_representation is _STREAM_HEARTBEAT_MARKER
|
|
1210
|
+
and not self.contents
|
|
1211
|
+
and self.role is None
|
|
1212
|
+
and self.author_name is None
|
|
1213
|
+
and self.response_id is None
|
|
1214
|
+
and self.message_id is None
|
|
1215
|
+
and self.conversation_id is None
|
|
1216
|
+
and self.model is None
|
|
1217
|
+
and self.created_at is None
|
|
1218
|
+
and self.finish_reason is None
|
|
1219
|
+
and self.continuation_token is None
|
|
1220
|
+
and not self.additional_properties
|
|
1221
|
+
)
|
|
1222
|
+
|
|
1223
|
+
@property
|
|
1224
|
+
def text(self) -> str:
|
|
1225
|
+
"""Returns the concatenated text of all contents in the update."""
|
|
1226
|
+
return "".join(_text_for_join(content) for content in self.contents if content.type == "text")
|
|
1227
|
+
|
|
1228
|
+
def __str__(self) -> str:
|
|
1229
|
+
return self.text
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
# region AgentResponse
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
class AgentResponse(SerializationMixin, Generic[ResponseModelT]):
|
|
1236
|
+
"""Represents the response to an Agent run request.
|
|
1237
|
+
|
|
1238
|
+
Provides one or more response messages and metadata about the response.
|
|
1239
|
+
A typical response will contain a single message, but may contain multiple
|
|
1240
|
+
messages in scenarios involving function calls, RAG retrievals, or complex logic.
|
|
1241
|
+
|
|
1242
|
+
Note:
|
|
1243
|
+
The `author_name` attribute is available on the `Message` objects inside `messages`,
|
|
1244
|
+
not on the `AgentResponse` itself. Use `response.messages[0].author_name` to access
|
|
1245
|
+
the author name of individual messages.
|
|
1246
|
+
|
|
1247
|
+
Examples:
|
|
1248
|
+
.. code-block:: python
|
|
1249
|
+
|
|
1250
|
+
from chrys.kernel._types import AgentResponse, Message
|
|
1251
|
+
|
|
1252
|
+
# Create agent response
|
|
1253
|
+
msg = Message("assistant", ["Task completed successfully."])
|
|
1254
|
+
response = AgentResponse(messages=[msg], response_id="run_123")
|
|
1255
|
+
print(response.text) # "Task completed successfully."
|
|
1256
|
+
|
|
1257
|
+
# Combine streaming updates
|
|
1258
|
+
updates = [...] # List of AgentResponseUpdate objects
|
|
1259
|
+
response = AgentResponse.from_updates(updates)
|
|
1260
|
+
|
|
1261
|
+
# Serialization - to_dict and from_dict
|
|
1262
|
+
response_dict = response.to_dict()
|
|
1263
|
+
# {'type': 'agent_response', 'messages': [...], 'response_id': 'run_123',
|
|
1264
|
+
# 'additional_properties': {}}
|
|
1265
|
+
restored_response = AgentResponse.from_dict(response_dict)
|
|
1266
|
+
print(restored_response.response_id) # "run_123"
|
|
1267
|
+
|
|
1268
|
+
# Serialization - to_json and from_json
|
|
1269
|
+
response_json = response.to_json()
|
|
1270
|
+
# '{"type": "agent_response", "messages": [...], "response_id": "run_123", ...}'
|
|
1271
|
+
restored_from_json = AgentResponse.from_json(response_json)
|
|
1272
|
+
print(restored_from_json.text) # "Task completed successfully."
|
|
1273
|
+
"""
|
|
1274
|
+
|
|
1275
|
+
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"raw_representation"}
|
|
1276
|
+
|
|
1277
|
+
def __init__(
|
|
1278
|
+
self,
|
|
1279
|
+
*,
|
|
1280
|
+
messages: Message | Sequence[Message] | None = None,
|
|
1281
|
+
response_id: str | None = None,
|
|
1282
|
+
agent_id: str | None = None,
|
|
1283
|
+
created_at: CreatedAtT | None = None,
|
|
1284
|
+
finish_reason: FinishReasonLiteral | FinishReason | None = None,
|
|
1285
|
+
usage_details: UsageDetails | None = None,
|
|
1286
|
+
latest_usage_details: UsageDetails | None = _UNSET_USAGE,
|
|
1287
|
+
value: ResponseModelT | None = None,
|
|
1288
|
+
response_format: StructuredResponseFormat = None,
|
|
1289
|
+
continuation_token: ContinuationToken | None = None,
|
|
1290
|
+
raw_representation: Any | None = None,
|
|
1291
|
+
additional_properties: dict[str, Any] | None = None,
|
|
1292
|
+
) -> None:
|
|
1293
|
+
"""Initialize an AgentResponse.
|
|
1294
|
+
|
|
1295
|
+
Keyword Args:
|
|
1296
|
+
messages: A single Message or sequence of Message objects to include in the response.
|
|
1297
|
+
response_id: The ID of the chat response.
|
|
1298
|
+
agent_id: The identifier of the agent that produced this response. Useful in multi-agent
|
|
1299
|
+
scenarios to track which agent generated the response.
|
|
1300
|
+
created_at: A timestamp for the chat response.
|
|
1301
|
+
finish_reason: The reason the model stopped generating. Common values include
|
|
1302
|
+
``"stop"`` (natural completion), ``"length"`` (token limit), and
|
|
1303
|
+
``"tool_calls"`` (the model invoked a tool).
|
|
1304
|
+
usage_details: Aggregate usage across all model calls in the agent run.
|
|
1305
|
+
latest_usage_details: Usage for the most recent model call.
|
|
1306
|
+
When omitted, defaults to ``usage_details`` (single-call response).
|
|
1307
|
+
Pass ``None`` explicitly when the final call's usage is unknown;
|
|
1308
|
+
it is preserved rather than falling back to the aggregate.
|
|
1309
|
+
value: The structured output of the agent run response, if applicable.
|
|
1310
|
+
response_format: Optional response format for the agent response.
|
|
1311
|
+
continuation_token: Optional token for resuming a long-running background operation.
|
|
1312
|
+
When present, indicates the operation is still in progress.
|
|
1313
|
+
additional_properties: Any additional properties associated with the chat response.
|
|
1314
|
+
raw_representation: The raw representation of the chat response from an underlying implementation.
|
|
1315
|
+
"""
|
|
1316
|
+
if messages is None:
|
|
1317
|
+
self.messages: list[Message] = []
|
|
1318
|
+
elif isinstance(messages, Message):
|
|
1319
|
+
self.messages = [messages]
|
|
1320
|
+
else:
|
|
1321
|
+
# Handle both Message objects and dicts (for from_dict support)
|
|
1322
|
+
processed_messages: list[Message] = []
|
|
1323
|
+
for msg in messages:
|
|
1324
|
+
if isinstance(msg, Message):
|
|
1325
|
+
processed_messages.append(msg)
|
|
1326
|
+
elif isinstance(msg, dict):
|
|
1327
|
+
processed_messages.append(Message.from_dict(msg))
|
|
1328
|
+
else:
|
|
1329
|
+
processed_messages.append(msg)
|
|
1330
|
+
self.messages = processed_messages
|
|
1331
|
+
self.response_id = response_id
|
|
1332
|
+
self.agent_id = agent_id
|
|
1333
|
+
self.created_at = created_at
|
|
1334
|
+
self.finish_reason = finish_reason
|
|
1335
|
+
self.usage_details = usage_details
|
|
1336
|
+
# Public attribute so serialization round-trips preserve the
|
|
1337
|
+
# occupancy view alongside the billing aggregate.
|
|
1338
|
+
self.latest_usage_details: UsageDetails | None = (
|
|
1339
|
+
usage_details if latest_usage_details is _UNSET_USAGE else latest_usage_details
|
|
1340
|
+
)
|
|
1341
|
+
self._value: ResponseModelT | None = value
|
|
1342
|
+
self._response_format: type[BaseModel] | Mapping[str, Any] | None = response_format
|
|
1343
|
+
self._value_parsed: bool = value is not None
|
|
1344
|
+
self.additional_properties = (
|
|
1345
|
+
_restore_compaction_annotation_in_additional_properties(additional_properties) or {}
|
|
1346
|
+
)
|
|
1347
|
+
self.continuation_token = continuation_token
|
|
1348
|
+
self.raw_representation = raw_representation
|
|
1349
|
+
# Runtime-only mirror of the inner ChatResponse's invalidation marker
|
|
1350
|
+
# (see ChatResponse.__init__); the agent's streaming post-hook reads it
|
|
1351
|
+
# to skip restoring a service session handle the loop invalidated.
|
|
1352
|
+
self._chrys_service_state_invalidated = False
|
|
1353
|
+
|
|
1354
|
+
@property
|
|
1355
|
+
def text(self) -> str:
|
|
1356
|
+
"""Get the concatenated text of all messages."""
|
|
1357
|
+
return "".join(msg.text for msg in self.messages) if self.messages else ""
|
|
1358
|
+
|
|
1359
|
+
@property
|
|
1360
|
+
def value(self) -> ResponseModelT | None:
|
|
1361
|
+
"""Get the parsed structured output value.
|
|
1362
|
+
|
|
1363
|
+
If a response_format was provided and parsing hasn't been attempted yet,
|
|
1364
|
+
this parses the final non-empty assistant message into the specified type.
|
|
1365
|
+
|
|
1366
|
+
Raises:
|
|
1367
|
+
ValidationError: If the response text doesn't match the expected schema.
|
|
1368
|
+
ValueError: If the response text is not valid JSON for a non-Pydantic structured format.
|
|
1369
|
+
"""
|
|
1370
|
+
if self._value_parsed:
|
|
1371
|
+
return self._value
|
|
1372
|
+
if self._response_format is not None:
|
|
1373
|
+
self._value = cast(
|
|
1374
|
+
ResponseModelT,
|
|
1375
|
+
_parse_structured_response_value(
|
|
1376
|
+
_last_non_empty_assistant_message_text(self.messages),
|
|
1377
|
+
self._response_format,
|
|
1378
|
+
),
|
|
1379
|
+
)
|
|
1380
|
+
self._value_parsed = True
|
|
1381
|
+
return self._value
|
|
1382
|
+
|
|
1383
|
+
def to_dict(self, *, exclude: set[str] | None = None, exclude_none: bool = True) -> dict[str, Any]:
|
|
1384
|
+
"""Serialize, preserving an explicitly-unavailable latest usage.
|
|
1385
|
+
|
|
1386
|
+
``exclude_none`` would drop ``latest_usage_details=None`` and the
|
|
1387
|
+
constructor default would then substitute the billing aggregate on
|
|
1388
|
+
deserialization. ``None`` is a meaningful value here (final call's
|
|
1389
|
+
usage unknown), so it is kept as an explicit ``null`` whenever an
|
|
1390
|
+
aggregate exists to be wrongly inherited.
|
|
1391
|
+
"""
|
|
1392
|
+
result = super().to_dict(exclude=exclude, exclude_none=exclude_none)
|
|
1393
|
+
if (
|
|
1394
|
+
exclude_none
|
|
1395
|
+
and self.latest_usage_details is None
|
|
1396
|
+
and "latest_usage_details" not in (exclude or ())
|
|
1397
|
+
and "usage_details" in result
|
|
1398
|
+
):
|
|
1399
|
+
result["latest_usage_details"] = None
|
|
1400
|
+
return result
|
|
1401
|
+
|
|
1402
|
+
@overload
|
|
1403
|
+
@classmethod
|
|
1404
|
+
def from_updates(
|
|
1405
|
+
cls: type[AgentResponse[Any]],
|
|
1406
|
+
updates: Sequence[AgentResponseUpdate],
|
|
1407
|
+
*,
|
|
1408
|
+
output_format_type: type[ResponseModelBoundT],
|
|
1409
|
+
value: Any | None = None,
|
|
1410
|
+
) -> AgentResponse[ResponseModelBoundT]: ...
|
|
1411
|
+
|
|
1412
|
+
@overload
|
|
1413
|
+
@classmethod
|
|
1414
|
+
def from_updates(
|
|
1415
|
+
cls: type[AgentResponse[Any]],
|
|
1416
|
+
updates: Sequence[AgentResponseUpdate],
|
|
1417
|
+
*,
|
|
1418
|
+
output_format_type: Mapping[str, Any],
|
|
1419
|
+
value: Any | None = None,
|
|
1420
|
+
) -> AgentResponse[Any]: ...
|
|
1421
|
+
|
|
1422
|
+
@overload
|
|
1423
|
+
@classmethod
|
|
1424
|
+
def from_updates(
|
|
1425
|
+
cls: type[AgentResponse[Any]],
|
|
1426
|
+
updates: Sequence[AgentResponseUpdate],
|
|
1427
|
+
*,
|
|
1428
|
+
output_format_type: None = None,
|
|
1429
|
+
value: Any | None = None,
|
|
1430
|
+
) -> AgentResponse[Any]: ...
|
|
1431
|
+
|
|
1432
|
+
@classmethod
|
|
1433
|
+
def from_updates(
|
|
1434
|
+
cls: type[AgentResponseT],
|
|
1435
|
+
updates: Sequence[AgentResponseUpdate],
|
|
1436
|
+
*,
|
|
1437
|
+
output_format_type: StructuredResponseFormat = None,
|
|
1438
|
+
value: Any | None = None,
|
|
1439
|
+
) -> AgentResponseT:
|
|
1440
|
+
"""Joins multiple updates into a single AgentResponse.
|
|
1441
|
+
|
|
1442
|
+
Args:
|
|
1443
|
+
updates: A sequence of AgentResponseUpdate objects to combine.
|
|
1444
|
+
|
|
1445
|
+
Keyword Args:
|
|
1446
|
+
output_format_type: Optional Pydantic model type or JSON schema mapping used to parse the
|
|
1447
|
+
response text into structured data.
|
|
1448
|
+
value: Optional pre-parsed structured output value to set directly on the response.
|
|
1449
|
+
"""
|
|
1450
|
+
msg = cls(messages=[], response_format=output_format_type, value=value)
|
|
1451
|
+
seen_contents: dict[int, Content] = {}
|
|
1452
|
+
for update in updates:
|
|
1453
|
+
_process_update(msg, update, seen_contents)
|
|
1454
|
+
_finalize_response(msg)
|
|
1455
|
+
return msg
|
|
1456
|
+
|
|
1457
|
+
@overload
|
|
1458
|
+
@classmethod
|
|
1459
|
+
async def from_update_generator(
|
|
1460
|
+
cls: type[AgentResponse[Any]],
|
|
1461
|
+
updates: AsyncIterable[AgentResponseUpdate],
|
|
1462
|
+
*,
|
|
1463
|
+
output_format_type: type[ResponseModelBoundT],
|
|
1464
|
+
) -> AgentResponse[ResponseModelBoundT]: ...
|
|
1465
|
+
|
|
1466
|
+
@overload
|
|
1467
|
+
@classmethod
|
|
1468
|
+
async def from_update_generator(
|
|
1469
|
+
cls: type[AgentResponse[Any]],
|
|
1470
|
+
updates: AsyncIterable[AgentResponseUpdate],
|
|
1471
|
+
*,
|
|
1472
|
+
output_format_type: Mapping[str, Any],
|
|
1473
|
+
) -> AgentResponse[Any]: ...
|
|
1474
|
+
|
|
1475
|
+
@overload
|
|
1476
|
+
@classmethod
|
|
1477
|
+
async def from_update_generator(
|
|
1478
|
+
cls: type[AgentResponse[Any]],
|
|
1479
|
+
updates: AsyncIterable[AgentResponseUpdate],
|
|
1480
|
+
*,
|
|
1481
|
+
output_format_type: None = None,
|
|
1482
|
+
) -> AgentResponse[Any]: ...
|
|
1483
|
+
|
|
1484
|
+
@classmethod
|
|
1485
|
+
async def from_update_generator(
|
|
1486
|
+
cls: type[AgentResponseT],
|
|
1487
|
+
updates: AsyncIterable[AgentResponseUpdate],
|
|
1488
|
+
*,
|
|
1489
|
+
output_format_type: StructuredResponseFormat = None,
|
|
1490
|
+
) -> AgentResponseT:
|
|
1491
|
+
"""Joins multiple updates into a single AgentResponse.
|
|
1492
|
+
|
|
1493
|
+
Args:
|
|
1494
|
+
updates: An async iterable of AgentResponseUpdate objects to combine.
|
|
1495
|
+
|
|
1496
|
+
Keyword Args:
|
|
1497
|
+
output_format_type: Optional Pydantic model type or JSON schema mapping used to parse the
|
|
1498
|
+
response text into structured data.
|
|
1499
|
+
"""
|
|
1500
|
+
msg = cls(messages=[], response_format=output_format_type)
|
|
1501
|
+
seen_contents: dict[int, Content] = {}
|
|
1502
|
+
async for update in updates:
|
|
1503
|
+
_process_update(msg, update, seen_contents)
|
|
1504
|
+
_finalize_response(msg)
|
|
1505
|
+
return msg
|
|
1506
|
+
|
|
1507
|
+
def __str__(self) -> str:
|
|
1508
|
+
return self.text
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
def _build_agent_response_from_chat_response(
|
|
1512
|
+
response: ChatResponse[Any],
|
|
1513
|
+
*,
|
|
1514
|
+
response_format: StructuredResponseFormat = None,
|
|
1515
|
+
) -> AgentResponse[Any]:
|
|
1516
|
+
"""Wrap a completed chat response without eagerly parsing structured output."""
|
|
1517
|
+
agent_response = AgentResponse(
|
|
1518
|
+
messages=response.messages,
|
|
1519
|
+
response_id=response.response_id,
|
|
1520
|
+
created_at=response.created_at,
|
|
1521
|
+
finish_reason=response.finish_reason,
|
|
1522
|
+
usage_details=response.usage_details,
|
|
1523
|
+
latest_usage_details=response.latest_usage_details,
|
|
1524
|
+
response_format=response_format,
|
|
1525
|
+
continuation_token=response.continuation_token,
|
|
1526
|
+
raw_representation=response,
|
|
1527
|
+
additional_properties=response.additional_properties,
|
|
1528
|
+
)
|
|
1529
|
+
if response._value_parsed:
|
|
1530
|
+
agent_response._value = response._value
|
|
1531
|
+
agent_response._value_parsed = True
|
|
1532
|
+
if response._chrys_service_state_invalidated:
|
|
1533
|
+
# Mirror the ephemeral invalidation marker so blocking callers see
|
|
1534
|
+
# the same no-continuation verdict the streaming finalizer stamps.
|
|
1535
|
+
agent_response._chrys_service_state_invalidated = True
|
|
1536
|
+
return agent_response
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
# region AgentResponseUpdate
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
class AgentResponseUpdate(SerializationMixin):
|
|
1543
|
+
"""Represents a single streaming response chunk from an Agent.
|
|
1544
|
+
|
|
1545
|
+
Attributes:
|
|
1546
|
+
contents: The content items in this update.
|
|
1547
|
+
role: The role of the author of the response update.
|
|
1548
|
+
author_name: The name of the author of the response update. In multi-agent scenarios,
|
|
1549
|
+
this identifies which agent generated this update. When updates are combined into
|
|
1550
|
+
an `AgentResponse`, the `author_name` is propagated to the resulting `Message` objects.
|
|
1551
|
+
agent_id: The identifier of the agent that produced this update. Useful in multi-agent
|
|
1552
|
+
scenarios to track which agent generated specific parts of the response.
|
|
1553
|
+
response_id: The ID of the response of which this update is a part.
|
|
1554
|
+
message_id: The ID of the message of which this update is a part.
|
|
1555
|
+
created_at: A timestamp for the response update.
|
|
1556
|
+
additional_properties: Any additional properties associated with the update.
|
|
1557
|
+
raw_representation: The raw representation from an underlying implementation.
|
|
1558
|
+
|
|
1559
|
+
Examples:
|
|
1560
|
+
.. code-block:: python
|
|
1561
|
+
|
|
1562
|
+
from chrys.kernel._content import Content
|
|
1563
|
+
from chrys.kernel._types import AgentResponseUpdate
|
|
1564
|
+
|
|
1565
|
+
# Create an agent run update
|
|
1566
|
+
update = AgentResponseUpdate(
|
|
1567
|
+
contents=[Content.from_text(text="Processing...")],
|
|
1568
|
+
role="assistant",
|
|
1569
|
+
response_id="run_123",
|
|
1570
|
+
)
|
|
1571
|
+
print(update.text) # "Processing..."
|
|
1572
|
+
|
|
1573
|
+
# Serialization - to_dict and from_dict
|
|
1574
|
+
update_dict = update.to_dict()
|
|
1575
|
+
# {'type': 'agent_response_update', 'contents': [{'type': 'text', 'text': 'Processing...'}],
|
|
1576
|
+
# 'role': 'assistant', 'response_id': 'run_123'}
|
|
1577
|
+
restored_update = AgentResponseUpdate.from_dict(update_dict)
|
|
1578
|
+
print(restored_update.response_id) # "run_123"
|
|
1579
|
+
|
|
1580
|
+
# Serialization - to_json and from_json
|
|
1581
|
+
update_json = update.to_json()
|
|
1582
|
+
# '{"type": "agent_response_update", "contents": [{"type": "text", "text": "Processing..."}], ...}'
|
|
1583
|
+
restored_from_json = AgentResponseUpdate.from_json(update_json)
|
|
1584
|
+
print(restored_from_json.text) # "Processing..."
|
|
1585
|
+
"""
|
|
1586
|
+
|
|
1587
|
+
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"raw_representation"}
|
|
1588
|
+
|
|
1589
|
+
def __init__(
|
|
1590
|
+
self,
|
|
1591
|
+
*,
|
|
1592
|
+
contents: Sequence[Content] | None = None,
|
|
1593
|
+
role: RoleLiteral | str | None = None,
|
|
1594
|
+
author_name: str | None = None,
|
|
1595
|
+
agent_id: str | None = None,
|
|
1596
|
+
response_id: str | None = None,
|
|
1597
|
+
message_id: str | None = None,
|
|
1598
|
+
created_at: CreatedAtT | None = None,
|
|
1599
|
+
finish_reason: FinishReasonLiteral | FinishReason | None = None,
|
|
1600
|
+
continuation_token: ContinuationToken | None = None,
|
|
1601
|
+
additional_properties: dict[str, Any] | None = None,
|
|
1602
|
+
raw_representation: Any | None = None,
|
|
1603
|
+
) -> None:
|
|
1604
|
+
"""Initialize an AgentResponseUpdate.
|
|
1605
|
+
|
|
1606
|
+
Keyword Args:
|
|
1607
|
+
contents: Optional list of Content items to include in the update.
|
|
1608
|
+
role: The role of the author of the response update (e.g., "user", "assistant").
|
|
1609
|
+
author_name: Optional name of the author of the response update. Used in multi-agent
|
|
1610
|
+
scenarios to identify which agent generated this update.
|
|
1611
|
+
agent_id: Optional identifier of the agent that produced this update.
|
|
1612
|
+
response_id: Optional ID of the response of which this update is a part.
|
|
1613
|
+
message_id: Optional ID of the message of which this update is a part.
|
|
1614
|
+
created_at: Optional timestamp for the chat response update.
|
|
1615
|
+
finish_reason: The reason the model stopped generating. Common values include
|
|
1616
|
+
``"stop"`` (natural completion), ``"length"`` (token limit), and
|
|
1617
|
+
``"tool_calls"`` (the model invoked a tool).
|
|
1618
|
+
continuation_token: Optional token for resuming a long-running background operation.
|
|
1619
|
+
When present, indicates the operation is still in progress.
|
|
1620
|
+
additional_properties: Optional additional properties associated with the chat response update.
|
|
1621
|
+
raw_representation: Optional raw representation of the chat response update.
|
|
1622
|
+
|
|
1623
|
+
"""
|
|
1624
|
+
# Handle contents - support dict conversion for from_dict
|
|
1625
|
+
if contents is None:
|
|
1626
|
+
self.contents: list[Content] = []
|
|
1627
|
+
else:
|
|
1628
|
+
processed_contents: list[Content] = []
|
|
1629
|
+
for c in contents:
|
|
1630
|
+
if isinstance(c, Content):
|
|
1631
|
+
processed_contents.append(c)
|
|
1632
|
+
elif isinstance(c, dict):
|
|
1633
|
+
processed_contents.append(Content.from_dict(c))
|
|
1634
|
+
else:
|
|
1635
|
+
processed_contents.append(c)
|
|
1636
|
+
self.contents = processed_contents
|
|
1637
|
+
|
|
1638
|
+
self.role: str | None = role
|
|
1639
|
+
self.author_name = author_name
|
|
1640
|
+
self.agent_id = agent_id
|
|
1641
|
+
self.response_id = response_id
|
|
1642
|
+
self.message_id = message_id
|
|
1643
|
+
self.created_at = created_at
|
|
1644
|
+
self.finish_reason = finish_reason
|
|
1645
|
+
self.continuation_token = continuation_token
|
|
1646
|
+
self.additional_properties = _restore_compaction_annotation_in_additional_properties(
|
|
1647
|
+
additional_properties,
|
|
1648
|
+
allow_none=True,
|
|
1649
|
+
)
|
|
1650
|
+
self.raw_representation: Any | list[Any] | None = raw_representation
|
|
1651
|
+
|
|
1652
|
+
@property
|
|
1653
|
+
def text(self) -> str:
|
|
1654
|
+
"""Get the concatenated text of all TextContent objects in contents."""
|
|
1655
|
+
return "".join(_text_for_join(content) for content in self.contents if content.type == "text")
|
|
1656
|
+
|
|
1657
|
+
def __str__(self) -> str:
|
|
1658
|
+
return self.text
|
|
1659
|
+
|
|
1660
|
+
|
|
1661
|
+
# region ResponseStream
|
|
1662
|
+
|
|
1663
|
+
|
|
1664
|
+
def map_chat_to_agent_update(update: ChatResponseUpdate, agent_name: str | None) -> AgentResponseUpdate:
|
|
1665
|
+
return AgentResponseUpdate(
|
|
1666
|
+
contents=update.contents,
|
|
1667
|
+
role=update.role,
|
|
1668
|
+
author_name=update.author_name or agent_name,
|
|
1669
|
+
response_id=update.response_id,
|
|
1670
|
+
message_id=update.message_id,
|
|
1671
|
+
created_at=update.created_at,
|
|
1672
|
+
finish_reason=update.finish_reason, # type: ignore[arg-type]
|
|
1673
|
+
continuation_token=update.continuation_token,
|
|
1674
|
+
additional_properties=update.additional_properties,
|
|
1675
|
+
raw_representation=update,
|
|
1676
|
+
)
|
|
1677
|
+
|
|
1678
|
+
|
|
1679
|
+
# Type variables for ResponseStream
|
|
1680
|
+
def _raise_stream_teardown_errors(errors: Sequence[BaseException]) -> None:
|
|
1681
|
+
"""Raise a collected teardown failure, preserving cancellation semantics."""
|
|
1682
|
+
for error in errors:
|
|
1683
|
+
if isinstance(error, asyncio.CancelledError):
|
|
1684
|
+
raise error
|
|
1685
|
+
raise errors[0]
|
|
1686
|
+
|
|
1687
|
+
|
|
1688
|
+
async def _resolve_maybe_awaitable[T](value: T | Awaitable[T]) -> T:
|
|
1689
|
+
"""Resolve a possibly-awaitable value without widening its result type."""
|
|
1690
|
+
if isawaitable(value):
|
|
1691
|
+
return cast("T", await value)
|
|
1692
|
+
return value
|
|
1693
|
+
|
|
1694
|
+
|
|
1695
|
+
async def _resolve_stream_source[T](
|
|
1696
|
+
source: AsyncIterable[T] | Awaitable[AsyncIterable[T]],
|
|
1697
|
+
) -> AsyncIterable[T]:
|
|
1698
|
+
"""Resolve the two supported stream-source shapes without losing ``T``."""
|
|
1699
|
+
if isinstance(source, AsyncIterable):
|
|
1700
|
+
return cast("AsyncIterable[T]", source)
|
|
1701
|
+
if not iscoroutine(source):
|
|
1702
|
+
return cast("AsyncIterable[T]", source)
|
|
1703
|
+
return await source
|
|
1704
|
+
|
|
1705
|
+
|
|
1706
|
+
@final
|
|
1707
|
+
class ResponseStream[UpdateT, FinalT](AsyncIterable[UpdateT]):
|
|
1708
|
+
"""Async stream wrapper that supports iteration and deferred finalization."""
|
|
1709
|
+
|
|
1710
|
+
def __init__(
|
|
1711
|
+
self,
|
|
1712
|
+
stream: AsyncIterable[UpdateT] | Awaitable[AsyncIterable[UpdateT]],
|
|
1713
|
+
*,
|
|
1714
|
+
finalizer: Callable[[Sequence[UpdateT]], FinalT | Awaitable[FinalT]] | None = None,
|
|
1715
|
+
transform_hooks: list[Callable[[UpdateT], UpdateT | Awaitable[UpdateT | None] | None]] | None = None,
|
|
1716
|
+
cleanup_hooks: list[Callable[[], Awaitable[None] | None]] | None = None,
|
|
1717
|
+
result_hooks: list[Callable[[FinalT], FinalT | Awaitable[FinalT | None] | None]] | None = None,
|
|
1718
|
+
) -> None:
|
|
1719
|
+
"""A Async Iterable stream of updates.
|
|
1720
|
+
|
|
1721
|
+
Args:
|
|
1722
|
+
stream: An async iterable or awaitable that resolves to an async iterable of updates.
|
|
1723
|
+
|
|
1724
|
+
Keyword Args:
|
|
1725
|
+
finalizer: An optional callable that takes the list of all updates and produces a final result.
|
|
1726
|
+
transform_hooks: Optional list of callables that transform each update as it is yielded.
|
|
1727
|
+
cleanup_hooks: Optional list of callables that run after the stream is fully consumed (before finalizer).
|
|
1728
|
+
result_hooks: Optional list of callables that transform the final result (after finalizer).
|
|
1729
|
+
|
|
1730
|
+
"""
|
|
1731
|
+
self._stream_source = stream
|
|
1732
|
+
self._finalizer = finalizer
|
|
1733
|
+
self._stream: AsyncIterable[UpdateT] | None = None
|
|
1734
|
+
self._iterator: AsyncIterator[UpdateT] | None = None
|
|
1735
|
+
self._updates: list[UpdateT] = []
|
|
1736
|
+
self._consumed: bool = False
|
|
1737
|
+
self._finalized: bool = False
|
|
1738
|
+
self._final_result: FinalT | list[UpdateT] | None = None
|
|
1739
|
+
self._transform_hooks: list[Callable[[UpdateT], UpdateT | Awaitable[UpdateT | None] | None]] = (
|
|
1740
|
+
transform_hooks if transform_hooks is not None else []
|
|
1741
|
+
)
|
|
1742
|
+
self._update_filters: list[Callable[[UpdateT], UpdateT]] = []
|
|
1743
|
+
self._result_hooks: list[Callable[[FinalT], FinalT | Awaitable[FinalT | None] | None]] = (
|
|
1744
|
+
result_hooks if result_hooks is not None else []
|
|
1745
|
+
)
|
|
1746
|
+
self._cleanup_hooks: list[Callable[[], Awaitable[None] | None]] = (
|
|
1747
|
+
cleanup_hooks if cleanup_hooks is not None else []
|
|
1748
|
+
)
|
|
1749
|
+
self._cleanup_run: bool = False
|
|
1750
|
+
self._closed: bool = False
|
|
1751
|
+
self._abandoned: bool = False
|
|
1752
|
+
self._stream_error: Exception | None = None
|
|
1753
|
+
self._inner_stream: ResponseStream[Any, Any] | None = None
|
|
1754
|
+
self._inner_stream_source: ResponseStream[Any, Any] | Awaitable[ResponseStream[Any, Any]] | None = None
|
|
1755
|
+
self._wrap_inner: bool = False
|
|
1756
|
+
self._map_update: Callable[[Any], UpdateT | Awaitable[UpdateT]] | None = None
|
|
1757
|
+
self._pull_context_manager_factories: list[Callable[[], contextlib.AbstractContextManager[Any]]] = []
|
|
1758
|
+
|
|
1759
|
+
@overload
|
|
1760
|
+
def map[OuterUpdateT, OuterFinalT](
|
|
1761
|
+
self,
|
|
1762
|
+
transform: Callable[[UpdateT], OuterUpdateT | Awaitable[OuterUpdateT]],
|
|
1763
|
+
finalizer: Callable[[Sequence[OuterUpdateT]], Awaitable[OuterFinalT]],
|
|
1764
|
+
) -> ResponseStream[OuterUpdateT, OuterFinalT]: ...
|
|
1765
|
+
|
|
1766
|
+
@overload
|
|
1767
|
+
def map[OuterUpdateT, OuterFinalT](
|
|
1768
|
+
self,
|
|
1769
|
+
transform: Callable[[UpdateT], OuterUpdateT | Awaitable[OuterUpdateT]],
|
|
1770
|
+
finalizer: Callable[[Sequence[OuterUpdateT]], OuterFinalT],
|
|
1771
|
+
) -> ResponseStream[OuterUpdateT, OuterFinalT]: ...
|
|
1772
|
+
|
|
1773
|
+
def map[OuterUpdateT, OuterFinalT](
|
|
1774
|
+
self,
|
|
1775
|
+
transform: Callable[[UpdateT], OuterUpdateT | Awaitable[OuterUpdateT]],
|
|
1776
|
+
finalizer: Callable[[Sequence[OuterUpdateT]], OuterFinalT | Awaitable[OuterFinalT]],
|
|
1777
|
+
) -> ResponseStream[OuterUpdateT, OuterFinalT]:
|
|
1778
|
+
"""Create a new stream that transforms each update.
|
|
1779
|
+
|
|
1780
|
+
The returned stream delegates iteration to this stream, ensuring single consumption.
|
|
1781
|
+
Each update is transformed by the provided function before being yielded.
|
|
1782
|
+
|
|
1783
|
+
Since the update type changes, a new finalizer MUST be provided that works with
|
|
1784
|
+
the transformed update type. The inner stream's finalizer cannot be used as it
|
|
1785
|
+
expects the original update type.
|
|
1786
|
+
|
|
1787
|
+
When ``get_final_response()`` is called on the mapped stream:
|
|
1788
|
+
1. The inner stream's finalizer runs first (on the original updates)
|
|
1789
|
+
2. The inner stream's result_hooks run (on the inner final result)
|
|
1790
|
+
3. The outer stream's finalizer runs (on the transformed updates)
|
|
1791
|
+
4. The outer stream's result_hooks run (on the outer final result)
|
|
1792
|
+
|
|
1793
|
+
This ensures that post-processing hooks registered on the inner stream (e.g.,
|
|
1794
|
+
context provider notifications, telemetry) are still executed.
|
|
1795
|
+
|
|
1796
|
+
Args:
|
|
1797
|
+
transform: Function to transform each update to a new type.
|
|
1798
|
+
finalizer: Function to convert collected (transformed) updates to the final type.
|
|
1799
|
+
This is required because the inner stream's finalizer won't work with
|
|
1800
|
+
the new update type.
|
|
1801
|
+
|
|
1802
|
+
Returns:
|
|
1803
|
+
A new ResponseStream with transformed update and final types.
|
|
1804
|
+
|
|
1805
|
+
Example:
|
|
1806
|
+
>>> chat_stream.map(
|
|
1807
|
+
... lambda u: AgentResponseUpdate(...),
|
|
1808
|
+
... AgentResponse.from_updates,
|
|
1809
|
+
... )
|
|
1810
|
+
"""
|
|
1811
|
+
stream = ResponseStream[OuterUpdateT, OuterFinalT](self, finalizer=finalizer)
|
|
1812
|
+
stream._inner_stream_source = self
|
|
1813
|
+
stream._wrap_inner = True
|
|
1814
|
+
stream._map_update = transform
|
|
1815
|
+
return stream
|
|
1816
|
+
|
|
1817
|
+
@overload
|
|
1818
|
+
def with_finalizer[OuterFinalT](
|
|
1819
|
+
self,
|
|
1820
|
+
finalizer: Callable[[Sequence[UpdateT]], Awaitable[OuterFinalT]],
|
|
1821
|
+
) -> ResponseStream[UpdateT, OuterFinalT]: ...
|
|
1822
|
+
|
|
1823
|
+
@overload
|
|
1824
|
+
def with_finalizer[OuterFinalT](
|
|
1825
|
+
self,
|
|
1826
|
+
finalizer: Callable[[Sequence[UpdateT]], OuterFinalT],
|
|
1827
|
+
) -> ResponseStream[UpdateT, OuterFinalT]: ...
|
|
1828
|
+
|
|
1829
|
+
def with_finalizer[OuterFinalT](
|
|
1830
|
+
self,
|
|
1831
|
+
finalizer: Callable[[Sequence[UpdateT]], OuterFinalT | Awaitable[OuterFinalT]],
|
|
1832
|
+
) -> ResponseStream[UpdateT, OuterFinalT]:
|
|
1833
|
+
"""Create a new stream with a different finalizer.
|
|
1834
|
+
|
|
1835
|
+
The returned stream delegates iteration to this stream, ensuring single consumption.
|
|
1836
|
+
When `get_final_response()` is called, the inner stream is finalized first,
|
|
1837
|
+
including its result hooks, and retains its cached final response. The new
|
|
1838
|
+
finalizer then receives the collected updates to produce the outer response.
|
|
1839
|
+
Each stream caches its own result; repeated retrieval does not repeat either
|
|
1840
|
+
finalizer or its result hooks.
|
|
1841
|
+
|
|
1842
|
+
Args:
|
|
1843
|
+
finalizer: Function to convert collected updates to the final response type.
|
|
1844
|
+
|
|
1845
|
+
Returns:
|
|
1846
|
+
A new ResponseStream with the new final type.
|
|
1847
|
+
|
|
1848
|
+
Example:
|
|
1849
|
+
>>> stream.with_finalizer(AgentResponse.from_updates)
|
|
1850
|
+
"""
|
|
1851
|
+
stream = ResponseStream[UpdateT, OuterFinalT](self, finalizer=finalizer)
|
|
1852
|
+
stream._inner_stream_source = self
|
|
1853
|
+
stream._wrap_inner = True
|
|
1854
|
+
return stream
|
|
1855
|
+
|
|
1856
|
+
@classmethod
|
|
1857
|
+
def from_awaitable(
|
|
1858
|
+
cls,
|
|
1859
|
+
awaitable: Awaitable[ResponseStream[UpdateT, FinalT]],
|
|
1860
|
+
) -> ResponseStream[UpdateT, FinalT]:
|
|
1861
|
+
"""Create a ResponseStream from an awaitable that resolves to a ResponseStream.
|
|
1862
|
+
|
|
1863
|
+
This is useful when you have an async function that returns a ResponseStream
|
|
1864
|
+
and you want to wrap it to add hooks or use it in a pipeline.
|
|
1865
|
+
|
|
1866
|
+
The returned stream delegates to the inner stream once it resolves, using the
|
|
1867
|
+
inner stream's finalizer if no new finalizer is provided.
|
|
1868
|
+
|
|
1869
|
+
Args:
|
|
1870
|
+
awaitable: An awaitable that resolves to a ResponseStream.
|
|
1871
|
+
|
|
1872
|
+
Returns:
|
|
1873
|
+
A new ResponseStream that wraps the awaitable.
|
|
1874
|
+
|
|
1875
|
+
Example:
|
|
1876
|
+
>>> async def get_stream() -> ResponseStream[Update, Response]: ...
|
|
1877
|
+
>>> stream = ResponseStream.from_awaitable(get_stream())
|
|
1878
|
+
"""
|
|
1879
|
+
stream: ResponseStream[UpdateT, FinalT] = cls(awaitable)
|
|
1880
|
+
stream._inner_stream_source = awaitable
|
|
1881
|
+
stream._wrap_inner = True
|
|
1882
|
+
return stream
|
|
1883
|
+
|
|
1884
|
+
async def _get_stream(self) -> AsyncIterable[UpdateT]:
|
|
1885
|
+
if self._stream is None:
|
|
1886
|
+
self._stream = await _resolve_stream_source(self._stream_source)
|
|
1887
|
+
if self._map_update is None and isinstance(self._stream, ResponseStream):
|
|
1888
|
+
# Push registered update filters down to the source stream so
|
|
1889
|
+
# they run before ITS accumulation — the innermost finalizer
|
|
1890
|
+
# is what produces the final response. Mapped streams keep
|
|
1891
|
+
# filters local: theirs expect the post-map update type.
|
|
1892
|
+
for update_filter in self._update_filters:
|
|
1893
|
+
self._stream.with_update_filter(update_filter)
|
|
1894
|
+
if isinstance(self._stream, ResponseStream) and self._wrap_inner:
|
|
1895
|
+
self._inner_stream = self._stream
|
|
1896
|
+
return self._inner_stream
|
|
1897
|
+
stream = self._stream
|
|
1898
|
+
if stream is None:
|
|
1899
|
+
raise RuntimeError("ResponseStream source did not resolve to an async iterable")
|
|
1900
|
+
return stream
|
|
1901
|
+
|
|
1902
|
+
def __aiter__(self) -> ResponseStream[UpdateT, FinalT]:
|
|
1903
|
+
return self
|
|
1904
|
+
|
|
1905
|
+
async def __anext__(self) -> UpdateT:
|
|
1906
|
+
if self._closed:
|
|
1907
|
+
raise StopAsyncIteration
|
|
1908
|
+
try:
|
|
1909
|
+
with contextlib.ExitStack() as stack:
|
|
1910
|
+
for factory in self._pull_context_manager_factories:
|
|
1911
|
+
stack.enter_context(factory())
|
|
1912
|
+
# Resolve the underlying stream inside the pull contexts so that any
|
|
1913
|
+
# spans/contexts created during stream resolution (e.g. inner chat
|
|
1914
|
+
# completion spans created on the first pull of a wrapped agent stream)
|
|
1915
|
+
# inherit the active context (e.g. an outer agent invoke span).
|
|
1916
|
+
if self._iterator is None:
|
|
1917
|
+
stream = await self._get_stream()
|
|
1918
|
+
self._iterator = stream.__aiter__()
|
|
1919
|
+
update: UpdateT = await self._iterator.__anext__()
|
|
1920
|
+
except StopAsyncIteration:
|
|
1921
|
+
self._consumed = True
|
|
1922
|
+
await self._run_cleanup_hooks()
|
|
1923
|
+
await self.get_final_response()
|
|
1924
|
+
raise
|
|
1925
|
+
except asyncio.CancelledError:
|
|
1926
|
+
try:
|
|
1927
|
+
await self.aclose()
|
|
1928
|
+
except Exception:
|
|
1929
|
+
logger.debug("ResponseStream close failed while cancelling a pull", exc_info=True)
|
|
1930
|
+
raise
|
|
1931
|
+
except Exception as exc:
|
|
1932
|
+
self._stream_error = exc
|
|
1933
|
+
try:
|
|
1934
|
+
await self._run_cleanup_hooks()
|
|
1935
|
+
finally:
|
|
1936
|
+
self._stream_error = None
|
|
1937
|
+
raise
|
|
1938
|
+
if self._map_update is not None:
|
|
1939
|
+
update = await _resolve_maybe_awaitable(self._map_update(update))
|
|
1940
|
+
if self._update_filters and (self._map_update is not None or not isinstance(self._stream, ResponseStream)):
|
|
1941
|
+
# Filters apply exactly once, at the level that pulls raw updates
|
|
1942
|
+
# (or locally after a map transform); a ResponseStream source
|
|
1943
|
+
# already applied them via push-down before accumulating.
|
|
1944
|
+
for update_filter in self._update_filters:
|
|
1945
|
+
update = update_filter(update)
|
|
1946
|
+
self._updates.append(update)
|
|
1947
|
+
for hook in self._transform_hooks:
|
|
1948
|
+
hooked = await _resolve_maybe_awaitable(hook(update))
|
|
1949
|
+
if hooked is not None:
|
|
1950
|
+
update = hooked
|
|
1951
|
+
return update
|
|
1952
|
+
|
|
1953
|
+
async def aclose(self) -> None:
|
|
1954
|
+
"""Close this stream and every resolved inner stream without finalizing.
|
|
1955
|
+
|
|
1956
|
+
Calling this method repeatedly is safe. An unresolved coroutine source
|
|
1957
|
+
is closed rather than awaited, so abandoning a lazy stream cannot start
|
|
1958
|
+
a provider request.
|
|
1959
|
+
"""
|
|
1960
|
+
if self._closed:
|
|
1961
|
+
return
|
|
1962
|
+
self._closed = True
|
|
1963
|
+
# A close before finalization abandons the partial updates: cleanup
|
|
1964
|
+
# hooks (and anything else) must never finalize them into a
|
|
1965
|
+
# successful-looking response or fire result hooks on them.
|
|
1966
|
+
if not self._finalized:
|
|
1967
|
+
self._abandoned = True
|
|
1968
|
+
|
|
1969
|
+
resolved: list[Any] = []
|
|
1970
|
+
if self._inner_stream is not None:
|
|
1971
|
+
resolved.append(self._inner_stream)
|
|
1972
|
+
if self._stream is not None:
|
|
1973
|
+
resolved.append(self._stream)
|
|
1974
|
+
if self._iterator is not None:
|
|
1975
|
+
resolved.append(self._iterator)
|
|
1976
|
+
|
|
1977
|
+
seen: set[int] = set()
|
|
1978
|
+
close_errors: list[BaseException] = []
|
|
1979
|
+
try:
|
|
1980
|
+
for item in resolved:
|
|
1981
|
+
if item is self or id(item) in seen:
|
|
1982
|
+
continue
|
|
1983
|
+
seen.add(id(item))
|
|
1984
|
+
try:
|
|
1985
|
+
if isinstance(item, ResponseStream):
|
|
1986
|
+
await item.aclose()
|
|
1987
|
+
continue
|
|
1988
|
+
close = getattr(item, "aclose", None)
|
|
1989
|
+
if close is not None:
|
|
1990
|
+
result = close()
|
|
1991
|
+
if isawaitable(result):
|
|
1992
|
+
await result
|
|
1993
|
+
except BaseException as exc:
|
|
1994
|
+
close_errors.append(exc)
|
|
1995
|
+
|
|
1996
|
+
if self._stream is None:
|
|
1997
|
+
sources = (self._stream_source, self._inner_stream_source)
|
|
1998
|
+
for source in sources:
|
|
1999
|
+
if source is None or id(source) in seen:
|
|
2000
|
+
continue
|
|
2001
|
+
seen.add(id(source))
|
|
2002
|
+
try:
|
|
2003
|
+
if iscoroutine(source):
|
|
2004
|
+
source.close()
|
|
2005
|
+
elif isinstance(source, asyncio.Future) and not source.done():
|
|
2006
|
+
source.cancel()
|
|
2007
|
+
else:
|
|
2008
|
+
close = getattr(source, "aclose", None)
|
|
2009
|
+
if close is not None:
|
|
2010
|
+
result = close()
|
|
2011
|
+
if isawaitable(result):
|
|
2012
|
+
await result
|
|
2013
|
+
except BaseException as exc:
|
|
2014
|
+
close_errors.append(exc)
|
|
2015
|
+
finally:
|
|
2016
|
+
try:
|
|
2017
|
+
await self._run_cleanup_hooks()
|
|
2018
|
+
except BaseException as exc:
|
|
2019
|
+
close_errors.append(exc)
|
|
2020
|
+
if close_errors:
|
|
2021
|
+
_raise_stream_teardown_errors(close_errors)
|
|
2022
|
+
|
|
2023
|
+
async def _resolve_stream_with_pull_contexts(self) -> AsyncIterable[UpdateT]:
|
|
2024
|
+
"""Resolve the underlying stream while activating any registered pull context managers.
|
|
2025
|
+
|
|
2026
|
+
Used by ``__await__`` and ``get_final_response`` so that any spans/contexts created
|
|
2027
|
+
during stream resolution (e.g. when the source is an Awaitable that internally
|
|
2028
|
+
creates child telemetry spans) inherit the same active context as iterator pulls.
|
|
2029
|
+
``__anext__`` resolves the stream inside its own ExitStack and so calls ``_get_stream``
|
|
2030
|
+
directly.
|
|
2031
|
+
"""
|
|
2032
|
+
if self._stream is not None:
|
|
2033
|
+
return await self._get_stream()
|
|
2034
|
+
with contextlib.ExitStack() as stack:
|
|
2035
|
+
for factory in self._pull_context_manager_factories:
|
|
2036
|
+
stack.enter_context(factory())
|
|
2037
|
+
return await self._get_stream()
|
|
2038
|
+
|
|
2039
|
+
def __await__(self) -> Generator[Any, None, ResponseStream[UpdateT, FinalT]]:
|
|
2040
|
+
async def _wrap() -> ResponseStream[UpdateT, FinalT]:
|
|
2041
|
+
await self._resolve_stream_with_pull_contexts()
|
|
2042
|
+
return self
|
|
2043
|
+
|
|
2044
|
+
return _wrap().__await__()
|
|
2045
|
+
|
|
2046
|
+
async def get_final_response(self) -> FinalT:
|
|
2047
|
+
"""Get the final response by applying the finalizer to all collected updates.
|
|
2048
|
+
|
|
2049
|
+
If a finalizer is configured, it receives the list of updates and returns the final type.
|
|
2050
|
+
Result hooks are then applied in order to transform the result.
|
|
2051
|
+
|
|
2052
|
+
If no finalizer is configured, returns the collected updates as Sequence[UpdateT].
|
|
2053
|
+
|
|
2054
|
+
For wrapped streams (created via .map() or .from_awaitable()):
|
|
2055
|
+
- The inner stream's finalizer is called first to produce the inner final result.
|
|
2056
|
+
- The inner stream's result_hooks are then applied to that inner result.
|
|
2057
|
+
- The outer stream's finalizer is called to convert the outer (mapped) updates to the final type.
|
|
2058
|
+
- The outer stream's result_hooks are then applied to transform the outer result.
|
|
2059
|
+
|
|
2060
|
+
This ensures that post-processing hooks registered on the inner stream (e.g., context
|
|
2061
|
+
provider notifications) are still executed even when the stream is wrapped/mapped.
|
|
2062
|
+
"""
|
|
2063
|
+
if self._abandoned and not self._finalized:
|
|
2064
|
+
raise RuntimeError("ResponseStream was closed before completion; no final response is available.")
|
|
2065
|
+
if self._wrap_inner:
|
|
2066
|
+
if self._inner_stream is None:
|
|
2067
|
+
# Use _resolve_stream_with_pull_contexts() so that any spans/contexts
|
|
2068
|
+
# created while resolving the awaitable (e.g. inner telemetry spans)
|
|
2069
|
+
# inherit the same active context as iterator pulls. This also handles
|
|
2070
|
+
# the case where _stream_source and _inner_stream_source are the same
|
|
2071
|
+
# coroutine (e.g., from from_awaitable), avoiding double-await errors.
|
|
2072
|
+
await self._resolve_stream_with_pull_contexts()
|
|
2073
|
+
if self._inner_stream is None:
|
|
2074
|
+
raise RuntimeError("Inner stream not available")
|
|
2075
|
+
if not self._finalized and not self._consumed:
|
|
2076
|
+
# Consume outer stream (which delegates to inner) if not already consumed
|
|
2077
|
+
async for _ in self:
|
|
2078
|
+
pass
|
|
2079
|
+
|
|
2080
|
+
# Re-check: __anext__ auto-finalization may have already finalized this stream
|
|
2081
|
+
if not self._finalized:
|
|
2082
|
+
# This ensures inner post-processing (e.g., context provider notifications) runs
|
|
2083
|
+
# Skip if inner stream was already finalized (e.g., via auto-finalization on iteration)
|
|
2084
|
+
if not self._inner_stream._finalized:
|
|
2085
|
+
inner_stream = self._inner_stream
|
|
2086
|
+
inner_result: Any
|
|
2087
|
+
if inner_stream._finalizer is not None:
|
|
2088
|
+
inner_finalizer = inner_stream._finalizer
|
|
2089
|
+
inner_result = await _resolve_maybe_awaitable(inner_finalizer(inner_stream._updates))
|
|
2090
|
+
else:
|
|
2091
|
+
inner_result = list(inner_stream._updates)
|
|
2092
|
+
|
|
2093
|
+
# Run inner stream's result hooks
|
|
2094
|
+
inner_hooks = cast(list[Callable[[Any], Any | Awaitable[Any] | None]], inner_stream._result_hooks)
|
|
2095
|
+
for hook in inner_hooks:
|
|
2096
|
+
hooked_result = await _resolve_maybe_awaitable(hook(inner_result))
|
|
2097
|
+
if hooked_result is not None:
|
|
2098
|
+
inner_result = hooked_result
|
|
2099
|
+
inner_stream._final_result = inner_result
|
|
2100
|
+
inner_stream._finalized = True
|
|
2101
|
+
else:
|
|
2102
|
+
inner_result = self._inner_stream._final_result
|
|
2103
|
+
|
|
2104
|
+
# Now finalize the outer stream with its own finalizer
|
|
2105
|
+
# If outer has no finalizer, use inner's result (preserves from_awaitable behavior)
|
|
2106
|
+
outer_result: Any
|
|
2107
|
+
if self._finalizer is not None:
|
|
2108
|
+
outer_result = await _resolve_maybe_awaitable(self._finalizer(self._updates))
|
|
2109
|
+
else:
|
|
2110
|
+
# No outer finalizer - use inner's finalized result
|
|
2111
|
+
outer_result = inner_result
|
|
2112
|
+
|
|
2113
|
+
# Apply outer's result_hooks
|
|
2114
|
+
outer_hooks = cast(list[Callable[[Any], Any | Awaitable[Any] | None]], self._result_hooks)
|
|
2115
|
+
for hook in outer_hooks:
|
|
2116
|
+
outer_hook_result = await _resolve_maybe_awaitable(hook(outer_result))
|
|
2117
|
+
if outer_hook_result is not None:
|
|
2118
|
+
outer_result = outer_hook_result
|
|
2119
|
+
self._final_result = outer_result
|
|
2120
|
+
self._finalized = True
|
|
2121
|
+
return cast("FinalT", self._final_result)
|
|
2122
|
+
|
|
2123
|
+
if not self._finalized and not self._consumed:
|
|
2124
|
+
async for _ in self:
|
|
2125
|
+
pass
|
|
2126
|
+
|
|
2127
|
+
# Re-check: __anext__ auto-finalization may have already finalized this stream
|
|
2128
|
+
if not self._finalized:
|
|
2129
|
+
result: Any
|
|
2130
|
+
if self._finalizer is not None:
|
|
2131
|
+
result = await _resolve_maybe_awaitable(self._finalizer(self._updates))
|
|
2132
|
+
else:
|
|
2133
|
+
result = list(self._updates)
|
|
2134
|
+
|
|
2135
|
+
final_hooks = cast(list[Callable[[Any], Any | Awaitable[Any] | None]], self._result_hooks)
|
|
2136
|
+
for hook in final_hooks:
|
|
2137
|
+
final_hook_result = await _resolve_maybe_awaitable(hook(result))
|
|
2138
|
+
if final_hook_result is not None:
|
|
2139
|
+
result = final_hook_result
|
|
2140
|
+
self._final_result = result
|
|
2141
|
+
self._finalized = True
|
|
2142
|
+
return cast("FinalT", self._final_result)
|
|
2143
|
+
|
|
2144
|
+
def with_transform_hook(
|
|
2145
|
+
self,
|
|
2146
|
+
hook: Callable[[UpdateT], UpdateT | Awaitable[UpdateT | None] | None],
|
|
2147
|
+
) -> ResponseStream[UpdateT, FinalT]:
|
|
2148
|
+
"""Register a transform hook executed for each update during iteration."""
|
|
2149
|
+
self._transform_hooks.append(hook)
|
|
2150
|
+
return self
|
|
2151
|
+
|
|
2152
|
+
def with_update_filter(
|
|
2153
|
+
self,
|
|
2154
|
+
update_filter: Callable[[UpdateT], UpdateT],
|
|
2155
|
+
) -> ResponseStream[UpdateT, FinalT]:
|
|
2156
|
+
"""Register a synchronous filter applied to each update BEFORE accumulation.
|
|
2157
|
+
|
|
2158
|
+
Transform hooks run after an update joins ``updates`` and shape only
|
|
2159
|
+
what consumers iterate; an update filter runs before accumulation, so
|
|
2160
|
+
the finalizer's assembly, every result hook, transform hooks, and
|
|
2161
|
+
iteration all observe the same filtered sequence. Wrapped streams
|
|
2162
|
+
push filters down to the innermost stream — whose finalizer produces
|
|
2163
|
+
the final response — and apply them exactly once, at the level that
|
|
2164
|
+
pulls from the raw update source (mapped streams apply theirs
|
|
2165
|
+
locally, after the map transform, matching the mapped update type).
|
|
2166
|
+
The filter must return an update; return a modified copy rather than
|
|
2167
|
+
mutating the argument, so producer-held aliases keep their shape.
|
|
2168
|
+
|
|
2169
|
+
Push-down follows ``ResponseStream`` sources only. A stream whose
|
|
2170
|
+
source is a plain async generator that itself drains ANOTHER stream
|
|
2171
|
+
and replays its updates (a semantic proxy, e.g. response-validation
|
|
2172
|
+
middleware) is a boundary push-down cannot cross — a filter
|
|
2173
|
+
registered on the proxy shapes only the replayed updates, never the
|
|
2174
|
+
drained stream's assembly. To reach beneath such proxies, deliver
|
|
2175
|
+
the filter on the request path instead
|
|
2176
|
+
(``ChatContext.stream_update_filters`` — the middleware pipeline
|
|
2177
|
+
attaches it to the stream its final handler resolves).
|
|
2178
|
+
"""
|
|
2179
|
+
self._update_filters.append(update_filter)
|
|
2180
|
+
if self._map_update is None and isinstance(self._stream, ResponseStream):
|
|
2181
|
+
self._stream.with_update_filter(update_filter)
|
|
2182
|
+
return self
|
|
2183
|
+
|
|
2184
|
+
def with_result_hook(
|
|
2185
|
+
self,
|
|
2186
|
+
hook: Callable[[FinalT], FinalT | Awaitable[FinalT | None] | None],
|
|
2187
|
+
) -> ResponseStream[UpdateT, FinalT]:
|
|
2188
|
+
"""Register a result hook executed after finalization."""
|
|
2189
|
+
self._result_hooks.append(hook)
|
|
2190
|
+
self._finalized = False
|
|
2191
|
+
self._final_result = None
|
|
2192
|
+
return self
|
|
2193
|
+
|
|
2194
|
+
def with_cleanup_hook(
|
|
2195
|
+
self,
|
|
2196
|
+
hook: Callable[[], Awaitable[None] | None],
|
|
2197
|
+
) -> ResponseStream[UpdateT, FinalT]:
|
|
2198
|
+
"""Register a cleanup hook executed after stream consumption (before finalizer)."""
|
|
2199
|
+
self._cleanup_hooks.append(hook)
|
|
2200
|
+
return self
|
|
2201
|
+
|
|
2202
|
+
def with_pull_context_manager(
|
|
2203
|
+
self,
|
|
2204
|
+
cm_factory: Callable[[], contextlib.AbstractContextManager[Any]],
|
|
2205
|
+
) -> ResponseStream[UpdateT, FinalT]:
|
|
2206
|
+
"""Register a context manager factory invoked around each underlying iterator pull.
|
|
2207
|
+
|
|
2208
|
+
The factory is called once per ``__anext__`` and the returned context manager wraps
|
|
2209
|
+
the await of the underlying iterator. This is useful for state that needs to be
|
|
2210
|
+
active while the inner async work runs - for example, attaching an OpenTelemetry
|
|
2211
|
+
span to the current context so child spans created by inner code (HTTP clients,
|
|
2212
|
+
tool execution) are correctly parented.
|
|
2213
|
+
|
|
2214
|
+
Because the context manager is entered and exited within the same ``__anext__``
|
|
2215
|
+
invocation, attach/detach style operations remain symmetric in the same async
|
|
2216
|
+
context regardless of where the stream is iterated.
|
|
2217
|
+
"""
|
|
2218
|
+
self._pull_context_manager_factories.append(cm_factory)
|
|
2219
|
+
return self
|
|
2220
|
+
|
|
2221
|
+
async def _run_cleanup_hooks(self) -> None:
|
|
2222
|
+
if self._cleanup_run:
|
|
2223
|
+
return
|
|
2224
|
+
self._cleanup_run = True
|
|
2225
|
+
hook_errors: list[BaseException] = []
|
|
2226
|
+
for hook in self._cleanup_hooks:
|
|
2227
|
+
try:
|
|
2228
|
+
result = hook()
|
|
2229
|
+
if isawaitable(result):
|
|
2230
|
+
await result
|
|
2231
|
+
except BaseException as exc:
|
|
2232
|
+
hook_errors.append(exc)
|
|
2233
|
+
if hook_errors:
|
|
2234
|
+
_raise_stream_teardown_errors(hook_errors)
|
|
2235
|
+
|
|
2236
|
+
@property
|
|
2237
|
+
def updates(self) -> Sequence[UpdateT]:
|
|
2238
|
+
return self._updates
|
|
2239
|
+
|
|
2240
|
+
|
|
2241
|
+
# region ChatOptions
|
|
2242
|
+
|
|
2243
|
+
|
|
2244
|
+
class ToolMode(TypedDict, total=False):
|
|
2245
|
+
"""Tool choice mode for the chat options.
|
|
2246
|
+
|
|
2247
|
+
Fields:
|
|
2248
|
+
mode: One of "auto", "required", or "none".
|
|
2249
|
+
required_function_name: Optional function name when `mode == "required"`.
|
|
2250
|
+
allowed_tools: Optional list of tool names when `mode` is `"auto"` or `"required"`.
|
|
2251
|
+
"""
|
|
2252
|
+
|
|
2253
|
+
mode: Literal["auto", "required", "none"]
|
|
2254
|
+
required_function_name: str
|
|
2255
|
+
allowed_tools: list[str]
|
|
2256
|
+
|
|
2257
|
+
|
|
2258
|
+
# region TypedDict-based Chat Options
|
|
2259
|
+
|
|
2260
|
+
|
|
2261
|
+
class _ChatOptionsBase(TypedDict, total=False):
|
|
2262
|
+
"""Common request settings for AI services as a TypedDict.
|
|
2263
|
+
|
|
2264
|
+
All fields are optional (total=False) to allow partial specification.
|
|
2265
|
+
Provider-specific TypedDicts extend this with additional options.
|
|
2266
|
+
|
|
2267
|
+
These options represent the common denominator across chat providers.
|
|
2268
|
+
Individual implementations may raise errors for unsupported options.
|
|
2269
|
+
|
|
2270
|
+
Examples:
|
|
2271
|
+
.. code-block:: python
|
|
2272
|
+
|
|
2273
|
+
from chrys.kernel._types import ChatOptions, ToolMode
|
|
2274
|
+
|
|
2275
|
+
# Type-safe options
|
|
2276
|
+
options: ChatOptions = {
|
|
2277
|
+
"temperature": 0.7,
|
|
2278
|
+
"max_tokens": 1000,
|
|
2279
|
+
"model": "gpt-4",
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
# With tools
|
|
2283
|
+
options_with_tools: ChatOptions = {
|
|
2284
|
+
"model": "gpt-4",
|
|
2285
|
+
"tool_choice": "auto",
|
|
2286
|
+
"temperature": 0.7,
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
# Used with Unpack for function signatures
|
|
2290
|
+
# async def get_response(self, **options: Unpack[ChatOptions]) -> ChatResponse:
|
|
2291
|
+
"""
|
|
2292
|
+
|
|
2293
|
+
# Model selection
|
|
2294
|
+
model: str
|
|
2295
|
+
# Generation parameters
|
|
2296
|
+
temperature: float
|
|
2297
|
+
top_p: float
|
|
2298
|
+
max_tokens: int
|
|
2299
|
+
stop: str | Sequence[str]
|
|
2300
|
+
seed: ReadOnly[int | None]
|
|
2301
|
+
logit_bias: ReadOnly[dict[str | int, float] | None]
|
|
2302
|
+
|
|
2303
|
+
# Penalty parameters
|
|
2304
|
+
frequency_penalty: ReadOnly[float | None]
|
|
2305
|
+
presence_penalty: ReadOnly[float | None]
|
|
2306
|
+
|
|
2307
|
+
# Tool configuration (forward reference to avoid circular import)
|
|
2308
|
+
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None
|
|
2309
|
+
tool_choice: ToolMode | Literal["auto", "required", "none"]
|
|
2310
|
+
allow_multiple_tool_calls: bool
|
|
2311
|
+
|
|
2312
|
+
# Metadata
|
|
2313
|
+
metadata: dict[str, Any]
|
|
2314
|
+
user: str
|
|
2315
|
+
store: ReadOnly[bool | None]
|
|
2316
|
+
conversation_id: ReadOnly[str | None]
|
|
2317
|
+
|
|
2318
|
+
# System/instructions
|
|
2319
|
+
instructions: str
|
|
2320
|
+
|
|
2321
|
+
|
|
2322
|
+
if TYPE_CHECKING:
|
|
2323
|
+
|
|
2324
|
+
class ChatOptions(_ChatOptionsBase, Generic[ResponseModelT], total=False):
|
|
2325
|
+
response_format: type[ResponseModelT] | Mapping[str, Any] | None
|
|
2326
|
+
|
|
2327
|
+
else:
|
|
2328
|
+
ChatOptions = _ChatOptionsBase
|
|
2329
|
+
|
|
2330
|
+
|
|
2331
|
+
# region Chat Options Utility Functions
|
|
2332
|
+
|
|
2333
|
+
|
|
2334
|
+
async def validate_chat_options(options: dict[str, Any]) -> dict[str, Any]:
|
|
2335
|
+
"""Validate and normalize chat options dictionary.
|
|
2336
|
+
|
|
2337
|
+
Validates numeric constraints and converts types as needed.
|
|
2338
|
+
|
|
2339
|
+
Args:
|
|
2340
|
+
options: The options dictionary to validate.
|
|
2341
|
+
|
|
2342
|
+
Returns:
|
|
2343
|
+
The validated and normalized options dictionary.
|
|
2344
|
+
|
|
2345
|
+
Raises:
|
|
2346
|
+
ValueError: If any option value is invalid.
|
|
2347
|
+
|
|
2348
|
+
Examples:
|
|
2349
|
+
.. code-block:: python
|
|
2350
|
+
|
|
2351
|
+
from chrys.kernel._types import validate_chat_options
|
|
2352
|
+
|
|
2353
|
+
options = await validate_chat_options({
|
|
2354
|
+
"temperature": 0.7,
|
|
2355
|
+
"max_tokens": 1000,
|
|
2356
|
+
})
|
|
2357
|
+
"""
|
|
2358
|
+
result = dict(options) # Make a copy
|
|
2359
|
+
|
|
2360
|
+
# Validate numeric constraints
|
|
2361
|
+
if (freq_pen := result.get("frequency_penalty")) is not None:
|
|
2362
|
+
if not (-2.0 <= freq_pen <= 2.0):
|
|
2363
|
+
raise ValueError("frequency_penalty must be between -2.0 and 2.0")
|
|
2364
|
+
result["frequency_penalty"] = float(freq_pen)
|
|
2365
|
+
|
|
2366
|
+
if (pres_pen := result.get("presence_penalty")) is not None:
|
|
2367
|
+
if not (-2.0 <= pres_pen <= 2.0):
|
|
2368
|
+
raise ValueError("presence_penalty must be between -2.0 and 2.0")
|
|
2369
|
+
result["presence_penalty"] = float(pres_pen)
|
|
2370
|
+
|
|
2371
|
+
if (temp := result.get("temperature")) is not None:
|
|
2372
|
+
if not (0.0 <= temp <= 2.0):
|
|
2373
|
+
raise ValueError("temperature must be between 0.0 and 2.0")
|
|
2374
|
+
result["temperature"] = float(temp)
|
|
2375
|
+
|
|
2376
|
+
if (top_p := result.get("top_p")) is not None:
|
|
2377
|
+
if not (0.0 <= top_p <= 1.0):
|
|
2378
|
+
raise ValueError("top_p must be between 0.0 and 1.0")
|
|
2379
|
+
result["top_p"] = float(top_p)
|
|
2380
|
+
|
|
2381
|
+
if (max_tokens := result.get("max_tokens")) is not None and max_tokens <= 0:
|
|
2382
|
+
raise ValueError("max_tokens must be greater than 0")
|
|
2383
|
+
|
|
2384
|
+
# Validate and normalize tools
|
|
2385
|
+
if "tools" in result:
|
|
2386
|
+
result["tools"] = await validate_tools(result["tools"])
|
|
2387
|
+
|
|
2388
|
+
return result
|
|
2389
|
+
|
|
2390
|
+
|
|
2391
|
+
def normalize_tools(
|
|
2392
|
+
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None,
|
|
2393
|
+
) -> list[ToolTypes]:
|
|
2394
|
+
"""Normalize tools into a list.
|
|
2395
|
+
|
|
2396
|
+
Converts callables to FunctionTool objects and preserves existing tool objects.
|
|
2397
|
+
|
|
2398
|
+
Args:
|
|
2399
|
+
tools: Tools to normalize - can be a single tool, callable, or sequence.
|
|
2400
|
+
|
|
2401
|
+
Returns:
|
|
2402
|
+
Normalized list of tools.
|
|
2403
|
+
|
|
2404
|
+
Examples:
|
|
2405
|
+
.. code-block:: python
|
|
2406
|
+
|
|
2407
|
+
from chrys.kernel import normalize_tools, tool
|
|
2408
|
+
|
|
2409
|
+
|
|
2410
|
+
@tool
|
|
2411
|
+
def my_tool(x: int) -> int:
|
|
2412
|
+
return x * 2
|
|
2413
|
+
|
|
2414
|
+
|
|
2415
|
+
# Single tool
|
|
2416
|
+
tools = normalize_tools(my_tool)
|
|
2417
|
+
|
|
2418
|
+
# List of tools
|
|
2419
|
+
tools = normalize_tools([my_tool, another_tool])
|
|
2420
|
+
"""
|
|
2421
|
+
from .tools import normalize_tools as _normalize_tools
|
|
2422
|
+
|
|
2423
|
+
return _normalize_tools(tools)
|
|
2424
|
+
|
|
2425
|
+
|
|
2426
|
+
async def validate_tools(
|
|
2427
|
+
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None,
|
|
2428
|
+
) -> list[ToolTypes]:
|
|
2429
|
+
"""Validate and normalize tools into a list.
|
|
2430
|
+
|
|
2431
|
+
Converts callables to FunctionTool objects, expands MCP tools to their constituent
|
|
2432
|
+
functions (connecting them if needed), while preserving non-callable tool objects.
|
|
2433
|
+
|
|
2434
|
+
Args:
|
|
2435
|
+
tools: Tools to validate - can be a single tool, callable, or sequence.
|
|
2436
|
+
|
|
2437
|
+
Returns:
|
|
2438
|
+
Normalized list of tools, or None if no tools provided.
|
|
2439
|
+
|
|
2440
|
+
Examples:
|
|
2441
|
+
.. code-block:: python
|
|
2442
|
+
|
|
2443
|
+
from chrys.kernel import tool
|
|
2444
|
+
from chrys.kernel._types import validate_tools
|
|
2445
|
+
|
|
2446
|
+
|
|
2447
|
+
@tool
|
|
2448
|
+
def my_tool(x: int) -> int:
|
|
2449
|
+
return x * 2
|
|
2450
|
+
|
|
2451
|
+
|
|
2452
|
+
# Single tool
|
|
2453
|
+
tools = await validate_tools(my_tool)
|
|
2454
|
+
|
|
2455
|
+
# List of tools
|
|
2456
|
+
tools = await validate_tools([my_tool, another_tool])
|
|
2457
|
+
"""
|
|
2458
|
+
# Use normalize_tools for common sync logic (converts callables to FunctionTool)
|
|
2459
|
+
normalized = normalize_tools(tools)
|
|
2460
|
+
|
|
2461
|
+
expander = _get_tool_expander()
|
|
2462
|
+
final_tools: list[ToolTypes] = []
|
|
2463
|
+
for tool_ in normalized:
|
|
2464
|
+
expanded = await expander(tool_) if expander is not None else None
|
|
2465
|
+
if expanded is None:
|
|
2466
|
+
final_tools.append(tool_)
|
|
2467
|
+
else:
|
|
2468
|
+
final_tools.extend(expanded)
|
|
2469
|
+
|
|
2470
|
+
return final_tools
|
|
2471
|
+
|
|
2472
|
+
|
|
2473
|
+
def validate_tool_mode(
|
|
2474
|
+
tool_choice: ToolMode | Literal["auto", "required", "none"] | None,
|
|
2475
|
+
) -> ToolMode | None:
|
|
2476
|
+
"""Validate and normalize tool_choice to a ToolMode dict.
|
|
2477
|
+
|
|
2478
|
+
Args:
|
|
2479
|
+
tool_choice: The tool choice value to validate.
|
|
2480
|
+
|
|
2481
|
+
Returns:
|
|
2482
|
+
A ToolMode dict (contains keys: "mode", and optionally
|
|
2483
|
+
"required_function_name" or "allowed_tools"), or ``None`` when not provided.
|
|
2484
|
+
|
|
2485
|
+
Raises:
|
|
2486
|
+
ContentError: If the tool_choice string is invalid.
|
|
2487
|
+
"""
|
|
2488
|
+
if tool_choice is None:
|
|
2489
|
+
return None
|
|
2490
|
+
if isinstance(tool_choice, str):
|
|
2491
|
+
if tool_choice not in ("auto", "required", "none"):
|
|
2492
|
+
raise ContentError(f"Invalid tool choice: {tool_choice}")
|
|
2493
|
+
return {"mode": tool_choice}
|
|
2494
|
+
if "mode" not in tool_choice:
|
|
2495
|
+
raise ContentError("tool_choice dict must contain 'mode' key")
|
|
2496
|
+
if tool_choice["mode"] not in ("auto", "required", "none"):
|
|
2497
|
+
raise ContentError(f"Invalid tool choice: {tool_choice['mode']}")
|
|
2498
|
+
if tool_choice["mode"] != "required" and "required_function_name" in tool_choice:
|
|
2499
|
+
raise ContentError("tool_choice with mode other than 'required' cannot have 'required_function_name'")
|
|
2500
|
+
if tool_choice["mode"] not in ("auto", "required") and "allowed_tools" in tool_choice:
|
|
2501
|
+
raise ContentError("tool_choice 'allowed_tools' is only valid when mode is 'auto' or 'required'")
|
|
2502
|
+
if "allowed_tools" in tool_choice:
|
|
2503
|
+
allowed_tools = tool_choice["allowed_tools"]
|
|
2504
|
+
if isinstance(allowed_tools, str) or not isinstance(allowed_tools, Sequence):
|
|
2505
|
+
raise ContentError("tool_choice 'allowed_tools' must be a non-string sequence of strings")
|
|
2506
|
+
if not all(isinstance(tool_name, str) for tool_name in allowed_tools):
|
|
2507
|
+
raise ContentError("tool_choice 'allowed_tools' must contain only strings")
|
|
2508
|
+
normalized_tool_choice = dict(tool_choice)
|
|
2509
|
+
normalized_tool_choice["allowed_tools"] = list(allowed_tools)
|
|
2510
|
+
return cast(ToolMode, normalized_tool_choice)
|
|
2511
|
+
return tool_choice
|
|
2512
|
+
|
|
2513
|
+
|
|
2514
|
+
def merge_chat_options(
|
|
2515
|
+
base: dict[str, Any] | None,
|
|
2516
|
+
override: dict[str, Any] | None,
|
|
2517
|
+
) -> dict[str, Any]:
|
|
2518
|
+
"""Merge two chat options dictionaries.
|
|
2519
|
+
|
|
2520
|
+
Values from override take precedence over base.
|
|
2521
|
+
Lists and dicts are combined (not replaced).
|
|
2522
|
+
Instructions are concatenated with newlines.
|
|
2523
|
+
|
|
2524
|
+
Args:
|
|
2525
|
+
base: The base options dictionary.
|
|
2526
|
+
override: The override options dictionary.
|
|
2527
|
+
|
|
2528
|
+
Returns:
|
|
2529
|
+
A new merged options dictionary.
|
|
2530
|
+
|
|
2531
|
+
Examples:
|
|
2532
|
+
.. code-block:: python
|
|
2533
|
+
|
|
2534
|
+
from chrys.kernel._types import merge_chat_options
|
|
2535
|
+
|
|
2536
|
+
base = {"temperature": 0.5, "model": "gpt-4"}
|
|
2537
|
+
override = {"temperature": 0.7, "max_tokens": 1000}
|
|
2538
|
+
merged = merge_chat_options(base, override)
|
|
2539
|
+
# {"temperature": 0.7, "model": "gpt-4", "max_tokens": 1000}
|
|
2540
|
+
"""
|
|
2541
|
+
if not base:
|
|
2542
|
+
return dict(override) if override else {}
|
|
2543
|
+
if not override:
|
|
2544
|
+
return dict(base)
|
|
2545
|
+
|
|
2546
|
+
result: dict[str, Any] = {}
|
|
2547
|
+
|
|
2548
|
+
# Copy base values (shallow copy for simple values, dict copy for dicts)
|
|
2549
|
+
for key, value in base.items():
|
|
2550
|
+
if isinstance(value, dict):
|
|
2551
|
+
result[key] = dict(value) # type: ignore[reportUnknownArgumentType]
|
|
2552
|
+
elif isinstance(value, list):
|
|
2553
|
+
result[key] = list(value) # type: ignore[reportUnknownArgumentType]
|
|
2554
|
+
else:
|
|
2555
|
+
result[key] = value
|
|
2556
|
+
|
|
2557
|
+
# Apply overrides
|
|
2558
|
+
for key, value in override.items():
|
|
2559
|
+
if value is None:
|
|
2560
|
+
continue
|
|
2561
|
+
|
|
2562
|
+
if key == "instructions":
|
|
2563
|
+
# Concatenate instructions
|
|
2564
|
+
base_instructions = result.get("instructions")
|
|
2565
|
+
if base_instructions:
|
|
2566
|
+
result["instructions"] = f"{base_instructions}\n{value}"
|
|
2567
|
+
else:
|
|
2568
|
+
result["instructions"] = value
|
|
2569
|
+
elif key == "tools":
|
|
2570
|
+
# Merge tools lists
|
|
2571
|
+
base_tools = result.get("tools")
|
|
2572
|
+
if base_tools and value:
|
|
2573
|
+
# Add tools that aren't already present
|
|
2574
|
+
merged_tools = list(base_tools)
|
|
2575
|
+
for tool in value if isinstance(value, Iterable) else [value]: # type: ignore[reportUnknownVariableType]
|
|
2576
|
+
if tool not in merged_tools:
|
|
2577
|
+
merged_tools.append(tool)
|
|
2578
|
+
result["tools"] = merged_tools
|
|
2579
|
+
elif value:
|
|
2580
|
+
result["tools"] = value if isinstance(value, list) else [value]
|
|
2581
|
+
elif key in ("logit_bias", "metadata", "additional_properties"):
|
|
2582
|
+
# Merge dicts
|
|
2583
|
+
base_dict = result.get(key)
|
|
2584
|
+
if base_dict and isinstance(base_dict, dict) and isinstance(value, dict):
|
|
2585
|
+
result[key] = {**base_dict, **value}
|
|
2586
|
+
elif value:
|
|
2587
|
+
result[key] = dict(cast(Mapping[Any, Any], value)) if isinstance(value, dict) else value
|
|
2588
|
+
elif key == "tool_choice":
|
|
2589
|
+
# tool_choice from override takes precedence
|
|
2590
|
+
result["tool_choice"] = value or result.get("tool_choice")
|
|
2591
|
+
elif key == "response_format":
|
|
2592
|
+
# response_format from override takes precedence if set
|
|
2593
|
+
result["response_format"] = value
|
|
2594
|
+
else:
|
|
2595
|
+
# Simple override
|
|
2596
|
+
result[key] = value
|
|
2597
|
+
|
|
2598
|
+
return result
|