iCode-TUI 0.29.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chrys/__init__.py +16 -0
- chrys/app/__init__.py +3 -0
- chrys/app/acp/__init__.py +3 -0
- chrys/app/acp/bridge.py +664 -0
- chrys/app/acp/content.py +132 -0
- chrys/app/acp/doc/frontend-api.md +425 -0
- chrys/app/acp/history.py +585 -0
- chrys/app/acp/server.py +1796 -0
- chrys/app/acp/session_manager.py +1815 -0
- chrys/app/acp/tool_status.py +45 -0
- chrys/app/cli/__init__.py +3 -0
- chrys/app/cli/acp.py +178 -0
- chrys/app/cli/app.py +165 -0
- chrys/app/cli/approvals.py +101 -0
- chrys/app/cli/headless.py +223 -0
- chrys/app/cli/launch_cwd.py +42 -0
- chrys/app/cli/profiles.py +298 -0
- chrys/app/cli/progress.py +857 -0
- chrys/app/cli/run.py +425 -0
- chrys/app/cli/serve.py +1593 -0
- chrys/app/cli/tool_summary.py +225 -0
- chrys/app/cli/trajectory.py +201 -0
- chrys/app/cli/workflow.py +601 -0
- chrys/app/features/__init__.py +3 -0
- chrys/app/features/buddy/__init__.py +3 -0
- chrys/app/features/buddy/actions.py +68 -0
- chrys/app/features/buddy/animation.py +76 -0
- chrys/app/features/buddy/commands.py +196 -0
- chrys/app/features/buddy/docs/design.md +187 -0
- chrys/app/features/buddy/hatchery.py +80 -0
- chrys/app/features/buddy/lifecycle.py +41 -0
- chrys/app/features/buddy/model.py +265 -0
- chrys/app/features/buddy/pixel_renderer.py +113 -0
- chrys/app/features/buddy/pixel_sprites.py +279 -0
- chrys/app/features/buddy/portrait.py +76 -0
- chrys/app/features/buddy/progression.py +58 -0
- chrys/app/features/buddy/replies.py +191 -0
- chrys/app/features/buddy/sprites/alien.toml +70 -0
- chrys/app/features/buddy/sprites/axolotl.toml +71 -0
- chrys/app/features/buddy/sprites/bat.toml +69 -0
- chrys/app/features/buddy/sprites/bee.toml +69 -0
- chrys/app/features/buddy/sprites/cactus.toml +73 -0
- chrys/app/features/buddy/sprites/capybara.toml +70 -0
- chrys/app/features/buddy/sprites/cat.toml +69 -0
- chrys/app/features/buddy/sprites/crab.toml +70 -0
- chrys/app/features/buddy/sprites/dragon.toml +71 -0
- chrys/app/features/buddy/sprites/duck.toml +69 -0
- chrys/app/features/buddy/sprites/fox.toml +69 -0
- chrys/app/features/buddy/sprites/frog.toml +70 -0
- chrys/app/features/buddy/sprites/ghost.toml +70 -0
- chrys/app/features/buddy/sprites/goose.toml +69 -0
- chrys/app/features/buddy/sprites/jelly.toml +70 -0
- chrys/app/features/buddy/sprites/llama.toml +70 -0
- chrys/app/features/buddy/sprites/mushroom.toml +69 -0
- chrys/app/features/buddy/sprites/octopus.toml +70 -0
- chrys/app/features/buddy/sprites/owl.toml +70 -0
- chrys/app/features/buddy/sprites/panda.toml +71 -0
- chrys/app/features/buddy/sprites/penguin.toml +69 -0
- chrys/app/features/buddy/sprites/rabbit.toml +70 -0
- chrys/app/features/buddy/sprites/robot.toml +72 -0
- chrys/app/features/buddy/sprites/shark.toml +70 -0
- chrys/app/features/buddy/sprites/snail.toml +69 -0
- chrys/app/features/buddy/sprites/snake.toml +69 -0
- chrys/app/features/buddy/sprites/tabby.toml +69 -0
- chrys/app/features/buddy/sprites/turtle.toml +69 -0
- chrys/app/features/buddy/store.py +143 -0
- chrys/app/features/session_title/__init__.py +7 -0
- chrys/app/features/session_title/updater.py +582 -0
- chrys/app/installer.py +651 -0
- chrys/app/parsing.py +17 -0
- chrys/app/tui/__init__.py +3 -0
- chrys/app/tui/app.py +1287 -0
- chrys/app/tui/behaviors/__init__.py +3 -0
- chrys/app/tui/behaviors/click_outside_dismiss.py +90 -0
- chrys/app/tui/behaviors/insert_clipboard.py +73 -0
- chrys/app/tui/behaviors/right_click_copy.py +146 -0
- chrys/app/tui/binding_display.py +139 -0
- chrys/app/tui/buddy_messages.py +17 -0
- chrys/app/tui/buddy_reply.py +86 -0
- chrys/app/tui/chrys.tcss +549 -0
- chrys/app/tui/clipboard.py +109 -0
- chrys/app/tui/copy_messages.py +10 -0
- chrys/app/tui/i18n.py +230 -0
- chrys/app/tui/language.py +58 -0
- chrys/app/tui/notifications/__init__.py +12 -0
- chrys/app/tui/notifications/drivers.py +180 -0
- chrys/app/tui/notifications/service.py +126 -0
- chrys/app/tui/notifications/settings.py +141 -0
- chrys/app/tui/screens/__init__.py +3 -0
- chrys/app/tui/screens/agents/__init__.py +8 -0
- chrys/app/tui/screens/agents/agent_draft_store.py +937 -0
- chrys/app/tui/screens/agents/config.py +2142 -0
- chrys/app/tui/screens/agents/config.tcss +169 -0
- chrys/app/tui/screens/agents/panels/__init__.py +3 -0
- chrys/app/tui/screens/agents/panels/acp.py +1176 -0
- chrys/app/tui/screens/agents/panels/basic.py +497 -0
- chrys/app/tui/screens/agents/panels/compaction.py +231 -0
- chrys/app/tui/screens/agents/panels/config_card.py +218 -0
- chrys/app/tui/screens/agents/panels/instructions.py +91 -0
- chrys/app/tui/screens/agents/panels/markdown_report.py +76 -0
- chrys/app/tui/screens/agents/panels/mcp.py +2262 -0
- chrys/app/tui/screens/agents/panels/memory.py +684 -0
- chrys/app/tui/screens/agents/panels/path_entry.py +227 -0
- chrys/app/tui/screens/agents/panels/skills.py +806 -0
- chrys/app/tui/screens/agents/panels/subagents.py +652 -0
- chrys/app/tui/screens/agents/panels/tools.py +205 -0
- chrys/app/tui/screens/agents/picker.py +103 -0
- chrys/app/tui/screens/agents/skill_paths.py +38 -0
- chrys/app/tui/screens/agents/validation_messages.py +452 -0
- chrys/app/tui/screens/dialogs/__init__.py +35 -0
- chrys/app/tui/screens/dialogs/agent_load.py +834 -0
- chrys/app/tui/screens/dialogs/agent_load.tcss +78 -0
- chrys/app/tui/screens/dialogs/app_mode.py +55 -0
- chrys/app/tui/screens/dialogs/approval/__init__.py +15 -0
- chrys/app/tui/screens/dialogs/approval/approval.tcss +311 -0
- chrys/app/tui/screens/dialogs/approval/bodies/__init__.py +3 -0
- chrys/app/tui/screens/dialogs/approval/bodies/file_edit.py +210 -0
- chrys/app/tui/screens/dialogs/approval/bodies/sub_agent.py +82 -0
- chrys/app/tui/screens/dialogs/approval/body.py +110 -0
- chrys/app/tui/screens/dialogs/approval/dialog.py +565 -0
- chrys/app/tui/screens/dialogs/approval/mode.py +105 -0
- chrys/app/tui/screens/dialogs/ask_user.py +148 -0
- chrys/app/tui/screens/dialogs/ask_user.tcss +62 -0
- chrys/app/tui/screens/dialogs/base.py +84 -0
- chrys/app/tui/screens/dialogs/confirm.py +184 -0
- chrys/app/tui/screens/dialogs/confirm.tcss +46 -0
- chrys/app/tui/screens/dialogs/connection_test.py +183 -0
- chrys/app/tui/screens/dialogs/connection_test.tcss +92 -0
- chrys/app/tui/screens/dialogs/diagram.py +102 -0
- chrys/app/tui/screens/dialogs/diagram.tcss +24 -0
- chrys/app/tui/screens/dialogs/editor.py +378 -0
- chrys/app/tui/screens/dialogs/editor.tcss +132 -0
- chrys/app/tui/screens/dialogs/file_picker.py +582 -0
- chrys/app/tui/screens/dialogs/file_picker.tcss +78 -0
- chrys/app/tui/screens/dialogs/fork_session.py +219 -0
- chrys/app/tui/screens/dialogs/fork_session.tcss +75 -0
- chrys/app/tui/screens/dialogs/image_compression.py +102 -0
- chrys/app/tui/screens/dialogs/image_compression.tcss +28 -0
- chrys/app/tui/screens/dialogs/man_page.py +172 -0
- chrys/app/tui/screens/dialogs/man_page.tcss +39 -0
- chrys/app/tui/screens/dialogs/new_folder.py +277 -0
- chrys/app/tui/screens/dialogs/new_folder.tcss +51 -0
- chrys/app/tui/screens/dialogs/notice.tcss +14 -0
- chrys/app/tui/screens/dialogs/prompt_cache.py +144 -0
- chrys/app/tui/screens/dialogs/prompt_cache.tcss +38 -0
- chrys/app/tui/screens/dialogs/runtime_details.py +371 -0
- chrys/app/tui/screens/dialogs/runtime_details.tcss +49 -0
- chrys/app/tui/screens/dialogs/session_title.py +110 -0
- chrys/app/tui/screens/dialogs/session_title.tcss +43 -0
- chrys/app/tui/screens/dialogs/tool_view.py +207 -0
- chrys/app/tui/screens/dialogs/tool_view.tcss +43 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.py +122 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.tcss +35 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.py +91 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.tcss +17 -0
- chrys/app/tui/screens/dialogs/workflow_input.py +258 -0
- chrys/app/tui/screens/dialogs/workflow_input.tcss +73 -0
- chrys/app/tui/screens/dialogs/workflow_node.py +462 -0
- chrys/app/tui/screens/dialogs/workflow_node.tcss +41 -0
- chrys/app/tui/screens/dialogs/workflow_picker.py +46 -0
- chrys/app/tui/screens/dialogs/workflow_result.py +136 -0
- chrys/app/tui/screens/dialogs/workflow_result.tcss +14 -0
- chrys/app/tui/screens/diff/__init__.py +22 -0
- chrys/app/tui/screens/diff/rollback_modal.py +1381 -0
- chrys/app/tui/screens/diff/rollback_modal.tcss +248 -0
- chrys/app/tui/screens/diff/screen.py +1532 -0
- chrys/app/tui/screens/diff/screen.tcss +104 -0
- chrys/app/tui/screens/guides/__init__.py +7 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/en/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/en/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/en/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/en/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/en/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/en/reference/workflows.md +1006 -0
- chrys/app/tui/screens/guides/_docs/en/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/en/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/_docs/index.yaml +100 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/workflows.md +1005 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/index.py +215 -0
- chrys/app/tui/screens/guides/reader.py +173 -0
- chrys/app/tui/screens/guides/screen.py +442 -0
- chrys/app/tui/screens/guides/screen.tcss +52 -0
- chrys/app/tui/screens/logs/__init__.py +7 -0
- chrys/app/tui/screens/logs/screen.py +504 -0
- chrys/app/tui/screens/logs/screen.tcss +45 -0
- chrys/app/tui/screens/main/__init__.py +7 -0
- chrys/app/tui/screens/main/action_availability.py +30 -0
- chrys/app/tui/screens/main/buddy_command.py +143 -0
- chrys/app/tui/screens/main/commands.py +1073 -0
- chrys/app/tui/screens/main/config_actions.py +464 -0
- chrys/app/tui/screens/main/copy_actions.py +117 -0
- chrys/app/tui/screens/main/dialog_controllers.py +826 -0
- chrys/app/tui/screens/main/dialog_gateway.py +166 -0
- chrys/app/tui/screens/main/diff_controller.py +478 -0
- chrys/app/tui/screens/main/engine_read_model.py +184 -0
- chrys/app/tui/screens/main/event_handlers.py +2014 -0
- chrys/app/tui/screens/main/input_flow.py +241 -0
- chrys/app/tui/screens/main/live_diff.py +244 -0
- chrys/app/tui/screens/main/main_screen_presenter.py +295 -0
- chrys/app/tui/screens/main/model_indicator.py +217 -0
- chrys/app/tui/screens/main/navigation.py +343 -0
- chrys/app/tui/screens/main/ports.py +571 -0
- chrys/app/tui/screens/main/recent_dirs.py +123 -0
- chrys/app/tui/screens/main/rollback_controller.py +586 -0
- chrys/app/tui/screens/main/runtime_info.py +71 -0
- chrys/app/tui/screens/main/screen.py +2265 -0
- chrys/app/tui/screens/main/screen.tcss +21 -0
- chrys/app/tui/screens/main/session_handlers.py +1127 -0
- chrys/app/tui/screens/main/session_title.py +239 -0
- chrys/app/tui/screens/main/settings_coordinator.py +365 -0
- chrys/app/tui/screens/main/settings_persistence.py +184 -0
- chrys/app/tui/screens/main/shell_mode.py +112 -0
- chrys/app/tui/screens/main/state.py +289 -0
- chrys/app/tui/screens/main/submit_state.py +11 -0
- chrys/app/tui/screens/main/subscriptions.py +177 -0
- chrys/app/tui/screens/main/suggestions.py +982 -0
- chrys/app/tui/screens/main/tool_action_bridge.py +75 -0
- chrys/app/tui/screens/main/view_adapter.py +1567 -0
- chrys/app/tui/screens/main/workflow_browser.py +747 -0
- chrys/app/tui/screens/main/workflow_content.py +248 -0
- chrys/app/tui/screens/main/workflow_controller.py +240 -0
- chrys/app/tui/screens/main/workflow_feedback.py +137 -0
- chrys/app/tui/screens/main/workflow_flow.py +49 -0
- chrys/app/tui/screens/main/workflow_run_control.py +456 -0
- chrys/app/tui/screens/main/workflow_session_view.py +316 -0
- chrys/app/tui/screens/main/workspace_actions.py +272 -0
- chrys/app/tui/screens/main/workspace_branch.py +194 -0
- chrys/app/tui/screens/menus/__init__.py +8 -0
- chrys/app/tui/screens/menus/languages.py +114 -0
- chrys/app/tui/screens/models/__init__.py +7 -0
- chrys/app/tui/screens/models/picker.py +147 -0
- chrys/app/tui/screens/models/screen.py +2043 -0
- chrys/app/tui/screens/models/screen.tcss +255 -0
- chrys/app/tui/screens/sessions/__init__.py +8 -0
- chrys/app/tui/screens/sessions/presenter.py +586 -0
- chrys/app/tui/screens/sessions/screen.py +779 -0
- chrys/app/tui/screens/sessions/screen.tcss +127 -0
- chrys/app/tui/screens/settings/__init__.py +29 -0
- chrys/app/tui/screens/settings/danger.py +26 -0
- chrys/app/tui/screens/settings/dialog.py +281 -0
- chrys/app/tui/screens/settings/layout.py +519 -0
- chrys/app/tui/screens/settings/migrate_dialog.py +253 -0
- chrys/app/tui/screens/settings/panes/__init__.py +3 -0
- chrys/app/tui/screens/settings/panes/notifications.py +248 -0
- chrys/app/tui/screens/settings/panes/sessions.py +148 -0
- chrys/app/tui/screens/settings/ports.py +55 -0
- chrys/app/tui/screens/settings/provenance.py +102 -0
- chrys/app/tui/screens/settings/rows.py +546 -0
- chrys/app/tui/screens/settings/settings.tcss +406 -0
- chrys/app/tui/screens/themes/__init__.py +8 -0
- chrys/app/tui/screens/themes/dialogs.py +330 -0
- chrys/app/tui/screens/themes/editor.py +632 -0
- chrys/app/tui/screens/themes/editor.tcss +268 -0
- chrys/app/tui/screens/themes/messages.py +95 -0
- chrys/app/tui/screens/themes/palette.py +694 -0
- chrys/app/tui/screens/themes/panel.py +113 -0
- chrys/app/tui/screens/themes/picker.py +203 -0
- chrys/app/tui/screens/themes/save.py +191 -0
- chrys/app/tui/support/__init__.py +3 -0
- chrys/app/tui/support/file_manager.py +64 -0
- chrys/app/tui/support/gc_freeze.py +1114 -0
- chrys/app/tui/support/path_shortcuts.py +50 -0
- chrys/app/tui/support/prompt_history.py +298 -0
- chrys/app/tui/support/workspace_mru.py +476 -0
- chrys/app/tui/terminal/__init__.py +6 -0
- chrys/app/tui/terminal/_pty_backend.py +114 -0
- chrys/app/tui/terminal/emulator/__init__.py +24 -0
- chrys/app/tui/terminal/emulator/buffer.py +456 -0
- chrys/app/tui/terminal/emulator/charsets.py +113 -0
- chrys/app/tui/terminal/emulator/core.py +963 -0
- chrys/app/tui/terminal/emulator/input.py +342 -0
- chrys/app/tui/terminal/emulator/parser.py +227 -0
- chrys/app/tui/terminal/emulator/pen.py +158 -0
- chrys/app/tui/terminal/launcher.py +455 -0
- chrys/app/tui/terminal/leaked_reports.py +44 -0
- chrys/app/tui/terminal/panel.py +323 -0
- chrys/app/tui/terminal/pty_process.py +405 -0
- chrys/app/tui/terminal/rendering.py +151 -0
- chrys/app/tui/terminal/shell.py +273 -0
- chrys/app/tui/terminal/shell_integration.py +199 -0
- chrys/app/tui/terminal/title.py +224 -0
- chrys/app/tui/terminal/widget.py +654 -0
- chrys/app/tui/theme.py +422 -0
- chrys/app/tui/theme_loader.py +201 -0
- chrys/app/tui/theme_stylesheet.py +49 -0
- chrys/app/tui/themes/__init__.py +3 -0
- chrys/app/tui/themes/document.py +120 -0
- chrys/app/tui/themes/preview.py +170 -0
- chrys/app/tui/themes/store.py +176 -0
- chrys/app/tui/toasts.py +69 -0
- chrys/app/tui/util/__init__.py +3 -0
- chrys/app/tui/util/context_pressure.py +146 -0
- chrys/app/tui/util/debug_log.py +292 -0
- chrys/app/tui/util/diff_entries.py +64 -0
- chrys/app/tui/util/formatting.py +53 -0
- chrys/app/tui/util/git_branch.py +326 -0
- chrys/app/tui/util/invocation_progress.py +57 -0
- chrys/app/tui/util/logo.py +32 -0
- chrys/app/tui/util/message_gate.py +35 -0
- chrys/app/tui/util/mutation_sources.py +14 -0
- chrys/app/tui/util/picker_text.py +27 -0
- chrys/app/tui/util/removal.py +47 -0
- chrys/app/tui/util/rich_style.py +36 -0
- chrys/app/tui/util/source_text.py +12 -0
- chrys/app/tui/util/static_update.py +88 -0
- chrys/app/tui/util/todo_format.py +51 -0
- chrys/app/tui/util/visibility.py +214 -0
- chrys/app/tui/widgets/__init__.py +60 -0
- chrys/app/tui/widgets/ask_user_controls.py +652 -0
- chrys/app/tui/widgets/ask_user_markdown.py +17 -0
- chrys/app/tui/widgets/ask_user_prompt.py +606 -0
- chrys/app/tui/widgets/buttons.py +52 -0
- chrys/app/tui/widgets/chat/__init__.py +3 -0
- chrys/app/tui/widgets/chat/agent_transcript_surface.py +818 -0
- chrys/app/tui/widgets/chat/chrome.py +296 -0
- chrys/app/tui/widgets/chat/compaction_card.py +353 -0
- chrys/app/tui/widgets/chat/context_fold.py +77 -0
- chrys/app/tui/widgets/chat/file_snapshot.py +91 -0
- chrys/app/tui/widgets/chat/image_preview.py +214 -0
- chrys/app/tui/widgets/chat/live_renderer.py +383 -0
- chrys/app/tui/widgets/chat/messages.py +1039 -0
- chrys/app/tui/widgets/chat/panel.py +1769 -0
- chrys/app/tui/widgets/chat/ports.py +245 -0
- chrys/app/tui/widgets/chat/renderers/__init__.py +7 -0
- chrys/app/tui/widgets/chat/renderers/ask_user.py +747 -0
- chrys/app/tui/widgets/chat/renderers/execute.py +431 -0
- chrys/app/tui/widgets/chat/renderers/file_edit.py +925 -0
- chrys/app/tui/widgets/chat/renderers/hosted_code.py +83 -0
- chrys/app/tui/widgets/chat/renderers/hosted_generic.py +330 -0
- chrys/app/tui/widgets/chat/renderers/hosted_image.py +107 -0
- chrys/app/tui/widgets/chat/renderers/hosted_mcp.py +43 -0
- chrys/app/tui/widgets/chat/renderers/hosted_search.py +167 -0
- chrys/app/tui/widgets/chat/renderers/hosted_shell.py +132 -0
- chrys/app/tui/widgets/chat/renderers/read_file.py +399 -0
- chrys/app/tui/widgets/chat/renderers/search.py +348 -0
- chrys/app/tui/widgets/chat/renderers/skill.py +564 -0
- chrys/app/tui/widgets/chat/renderers/sleep.py +287 -0
- chrys/app/tui/widgets/chat/renderers/sub_agent.py +1385 -0
- chrys/app/tui/widgets/chat/renderers/todo.py +114 -0
- chrys/app/tui/widgets/chat/renderers/web.py +69 -0
- chrys/app/tui/widgets/chat/replay.py +1605 -0
- chrys/app/tui/widgets/chat/replay_mount.py +322 -0
- chrys/app/tui/widgets/chat/scroll_controller.py +579 -0
- chrys/app/tui/widgets/chat/selection_controller.py +435 -0
- chrys/app/tui/widgets/chat/session_json.py +508 -0
- chrys/app/tui/widgets/chat/toc_model.py +156 -0
- chrys/app/tui/widgets/chat/tool_call.py +2478 -0
- chrys/app/tui/widgets/chat/tool_registry.py +753 -0
- chrys/app/tui/widgets/chat/tool_renderers.py +247 -0
- chrys/app/tui/widgets/chat/tool_view_builders.py +260 -0
- chrys/app/tui/widgets/chat/transcript_reader.py +57 -0
- chrys/app/tui/widgets/checkbox.py +24 -0
- chrys/app/tui/widgets/chrome/__init__.py +3 -0
- chrys/app/tui/widgets/chrome/app_header.py +283 -0
- chrys/app/tui/widgets/chrome/commands.py +95 -0
- chrys/app/tui/widgets/chrome/file_index.py +68 -0
- chrys/app/tui/widgets/chrome/file_scanner.py +670 -0
- chrys/app/tui/widgets/chrome/footer.py +284 -0
- chrys/app/tui/widgets/chrome/image_paste.py +351 -0
- chrys/app/tui/widgets/chrome/input_bar.py +1299 -0
- chrys/app/tui/widgets/chrome/status_bar.py +911 -0
- chrys/app/tui/widgets/chrome/suggestion_list.py +652 -0
- chrys/app/tui/widgets/click_affordance.py +24 -0
- chrys/app/tui/widgets/color_picker/__init__.py +254 -0
- chrys/app/tui/widgets/color_picker/controls.py +171 -0
- chrys/app/tui/widgets/color_picker/model.py +89 -0
- chrys/app/tui/widgets/dialog_buttons.py +58 -0
- chrys/app/tui/widgets/dialog_scroll.py +24 -0
- chrys/app/tui/widgets/diff_view/__init__.py +20 -0
- chrys/app/tui/widgets/diff_view/cells.py +59 -0
- chrys/app/tui/widgets/diff_view/code.py +280 -0
- chrys/app/tui/widgets/diff_view/compute.py +286 -0
- chrys/app/tui/widgets/diff_view/gutter.py +161 -0
- chrys/app/tui/widgets/diff_view/inline.py +77 -0
- chrys/app/tui/widgets/diff_view/palette.py +87 -0
- chrys/app/tui/widgets/diff_view/protocols.py +24 -0
- chrys/app/tui/widgets/diff_view/rows.py +138 -0
- chrys/app/tui/widgets/diff_view/unified.py +267 -0
- chrys/app/tui/widgets/diff_view/widget.py +209 -0
- chrys/app/tui/widgets/editor/__init__.py +79 -0
- chrys/app/tui/widgets/editor/coordinates.py +30 -0
- chrys/app/tui/widgets/editor/highlighter.py +139 -0
- chrys/app/tui/widgets/editor/keymaps.py +357 -0
- chrys/app/tui/widgets/editor/types.py +216 -0
- chrys/app/tui/widgets/editor/vim.py +1195 -0
- chrys/app/tui/widgets/editor/widget.py +401 -0
- chrys/app/tui/widgets/file_preview.py +58 -0
- chrys/app/tui/widgets/hatch.py +138 -0
- chrys/app/tui/widgets/input.py +66 -0
- chrys/app/tui/widgets/loading.py +125 -0
- chrys/app/tui/widgets/markdown/__init__.py +25 -0
- chrys/app/tui/widgets/markdown/_utils.py +21 -0
- chrys/app/tui/widgets/markdown/anchors.py +71 -0
- chrys/app/tui/widgets/markdown/blocks.py +115 -0
- chrys/app/tui/widgets/markdown/copyable.py +163 -0
- chrys/app/tui/widgets/markdown/diagram/__init__.py +35 -0
- chrys/app/tui/widgets/markdown/diagram/canvas.py +409 -0
- chrys/app/tui/widgets/markdown/diagram/charts.py +52 -0
- chrys/app/tui/widgets/markdown/diagram/diagnostics.py +101 -0
- chrys/app/tui/widgets/markdown/diagram/geometry.py +46 -0
- chrys/app/tui/widgets/markdown/diagram/layout.py +494 -0
- chrys/app/tui/widgets/markdown/diagram/messages.py +18 -0
- chrys/app/tui/widgets/markdown/diagram/model.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/parser.py +173 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/charts.py +349 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/common.py +403 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/graphs.py +1072 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/planning.py +317 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schedule.py +298 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schemas.py +241 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/structure.py +221 -0
- chrys/app/tui/widgets/markdown/diagram/presentation.py +32 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/charts.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/common.py +73 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/planning.py +157 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/schedule.py +202 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/structure.py +77 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/xy.py +385 -0
- chrys/app/tui/widgets/markdown/diagram/router.py +651 -0
- chrys/app/tui/widgets/markdown/diagram/specs/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/specs/charts.py +94 -0
- chrys/app/tui/widgets/markdown/diagram/specs/planning.py +68 -0
- chrys/app/tui/widgets/markdown/diagram/specs/schedule.py +65 -0
- chrys/app/tui/widgets/markdown/diagram/specs/structure.py +23 -0
- chrys/app/tui/widgets/markdown/diagram/viewer.py +92 -0
- chrys/app/tui/widgets/markdown/links.py +47 -0
- chrys/app/tui/widgets/markdown/math/__init__.py +36 -0
- chrys/app/tui/widgets/markdown/math/layout.py +739 -0
- chrys/app/tui/widgets/markdown/math/markdown.py +291 -0
- chrys/app/tui/widgets/markdown/math/parser.py +325 -0
- chrys/app/tui/widgets/markdown/math/symbols.py +325 -0
- chrys/app/tui/widgets/markdown/navigator.py +80 -0
- chrys/app/tui/widgets/markdown/parser.py +1057 -0
- chrys/app/tui/widgets/markdown/stream.py +73 -0
- chrys/app/tui/widgets/markdown/toc.py +98 -0
- chrys/app/tui/widgets/markdown/viewer.py +146 -0
- chrys/app/tui/widgets/markdown/widget.py +2046 -0
- chrys/app/tui/widgets/marquee.py +101 -0
- chrys/app/tui/widgets/option_menu.py +82 -0
- chrys/app/tui/widgets/page_navigator.py +118 -0
- chrys/app/tui/widgets/select.py +196 -0
- chrys/app/tui/widgets/selection.py +59 -0
- chrys/app/tui/widgets/sidebar/__init__.py +3 -0
- chrys/app/tui/widgets/sidebar/buddy.py +367 -0
- chrys/app/tui/widgets/sidebar/context.py +482 -0
- chrys/app/tui/widgets/sidebar/debug.py +195 -0
- chrys/app/tui/widgets/sidebar/empty_state.py +11 -0
- chrys/app/tui/widgets/sidebar/panel.py +198 -0
- chrys/app/tui/widgets/sidebar/tasks.py +198 -0
- chrys/app/tui/widgets/sidebar/toc.py +166 -0
- chrys/app/tui/widgets/sidebar/toc_types.py +30 -0
- chrys/app/tui/widgets/syntax_theme.py +85 -0
- chrys/app/tui/widgets/text_area.py +258 -0
- chrys/app/tui/widgets/trajectory/__init__.py +8 -0
- chrys/app/tui/widgets/trajectory/chartkit.py +264 -0
- chrys/app/tui/widgets/trajectory/insights.py +1266 -0
- chrys/app/tui/widgets/trajectory/overview.py +1101 -0
- chrys/app/tui/widgets/trajectory/panel.py +983 -0
- chrys/app/tui/widgets/trajectory/presentation.py +311 -0
- chrys/app/tui/widgets/trajectory/session_info.py +124 -0
- chrys/app/tui/widgets/trajectory/text_view.py +124 -0
- chrys/app/tui/widgets/trajectory/timeline.py +380 -0
- chrys/app/tui/widgets/welcome.py +126 -0
- chrys/app/tui/widgets/workflow/__init__.py +5 -0
- chrys/app/tui/widgets/workflow/edges.py +89 -0
- chrys/app/tui/widgets/workflow/graph.py +869 -0
- chrys/app/tui/widgets/workflow/info.py +310 -0
- chrys/app/tui/widgets/workflow/node_view.py +27 -0
- chrys/app/tui/widgets/workflow/output.py +225 -0
- chrys/app/tui/widgets/workflow/panel.py +658 -0
- chrys/app/tui/widgets/workflow/projector.py +324 -0
- chrys/app/tui/widgets/workflow/records.py +69 -0
- chrys/app/tui/widgets/workflow/scrollbar.py +82 -0
- chrys/app/tui/widgets/workflow/selection.py +151 -0
- chrys/app/tui/widgets/workflow/source.py +65 -0
- chrys/app/tui/widgets/workflow/text.py +257 -0
- chrys/app/tui/widgets/workflow/values.py +317 -0
- chrys/foundation/__init__.py +0 -0
- chrys/foundation/branding.py +15 -0
- chrys/foundation/config/__init__.py +7 -0
- chrys/foundation/config/coercion.py +374 -0
- chrys/foundation/config/context.py +34 -0
- chrys/foundation/config/env_file.py +141 -0
- chrys/foundation/config/env_layers.py +370 -0
- chrys/foundation/config/migrations.py +521 -0
- chrys/foundation/config/notification_events.py +22 -0
- chrys/foundation/config/process_settings.py +332 -0
- chrys/foundation/config/runtime_pointer.py +133 -0
- chrys/foundation/config/settings.py +1952 -0
- chrys/foundation/config/settings_store.py +996 -0
- chrys/foundation/config/spec.py +396 -0
- chrys/foundation/config/user_settings.py +140 -0
- chrys/foundation/config/warnings.py +262 -0
- chrys/foundation/config/web_values.py +135 -0
- chrys/foundation/config/yaml_store.py +487 -0
- chrys/foundation/errors/__init__.py +68 -0
- chrys/foundation/errors/_legacy.py +117 -0
- chrys/foundation/errors/_text.py +13 -0
- chrys/foundation/errors/_walk.py +85 -0
- chrys/foundation/errors/classify.py +278 -0
- chrys/foundation/errors/display.py +274 -0
- chrys/foundation/errors/formatting.py +283 -0
- chrys/foundation/errors/kinds.py +67 -0
- chrys/foundation/errors/network.py +497 -0
- chrys/foundation/errors/provider.py +445 -0
- chrys/foundation/errors/route.py +85 -0
- chrys/foundation/events/__init__.py +8 -0
- chrys/foundation/events/bus.py +143 -0
- chrys/foundation/events/types.py +1629 -0
- chrys/foundation/events/workflow.py +38 -0
- chrys/foundation/hosted_tools.py +142 -0
- chrys/foundation/i18n/__init__.py +32 -0
- chrys/foundation/i18n/_catalogs/zh-Hans/LC_MESSAGES/chrys.mo +0 -0
- chrys/foundation/i18n/catalogs.py +42 -0
- chrys/foundation/i18n/formatting.py +202 -0
- chrys/foundation/i18n/locale.py +92 -0
- chrys/foundation/i18n/localizer.py +187 -0
- chrys/foundation/i18n/messages.py +268 -0
- chrys/foundation/io/__init__.py +3 -0
- chrys/foundation/io/result_content.py +237 -0
- chrys/foundation/models/__init__.py +3 -0
- chrys/foundation/models/approval_reuse.py +23 -0
- chrys/foundation/models/ask_user.py +468 -0
- chrys/foundation/models/execution.py +18 -0
- chrys/foundation/models/history_markers.py +115 -0
- chrys/foundation/models/invocations.py +63 -0
- chrys/foundation/models/mutation_scope.py +28 -0
- chrys/foundation/models/session_env.py +87 -0
- chrys/foundation/models/session_surface.py +29 -0
- chrys/foundation/models/todos.py +74 -0
- chrys/foundation/models/turns.py +125 -0
- chrys/foundation/models/workflow_session.py +89 -0
- chrys/foundation/models/workspace.py +59 -0
- chrys/foundation/net/__init__.py +3 -0
- chrys/foundation/net/address_class.py +57 -0
- chrys/foundation/net/route_probe.py +94 -0
- chrys/foundation/net/url.py +107 -0
- chrys/foundation/observability/__init__.py +3 -0
- chrys/foundation/observability/exporters.py +174 -0
- chrys/foundation/observability/gate.py +31 -0
- chrys/foundation/observability/setup.py +497 -0
- chrys/foundation/observability/sink.py +228 -0
- chrys/foundation/patches/__init__.py +19 -0
- chrys/foundation/patches/mdurl_cache.py +59 -0
- chrys/foundation/patches/patcher.py +287 -0
- chrys/foundation/patches/staged_members.py +205 -0
- chrys/foundation/patches/textual_block_border.py +105 -0
- chrys/foundation/patches/textual_button_ansi.py +65 -0
- chrys/foundation/patches/textual_button_ansi_old.txt +68 -0
- chrys/foundation/patches/textual_callback_cache.py +248 -0
- chrys/foundation/patches/textual_callback_dispatch.py +109 -0
- chrys/foundation/patches/textual_compositor_cjk.py +566 -0
- chrys/foundation/patches/textual_dispatch_cache.py +150 -0
- chrys/foundation/patches/textual_ime_cursor_anchor.py +92 -0
- chrys/foundation/patches/textual_kitty_keyboard.py +268 -0
- chrys/foundation/patches/textual_lru_acyclic.py +241 -0
- chrys/foundation/patches/textual_message_pump.py +136 -0
- chrys/foundation/patches/textual_node_diet.py +875 -0
- chrys/foundation/patches/textual_one_shot_timer.py +48 -0
- chrys/foundation/patches/textual_option_list.py +90 -0
- chrys/foundation/patches/textual_precompose.py +89 -0
- chrys/foundation/patches/textual_pruned_tabs.py +69 -0
- chrys/foundation/patches/textual_reflow_reuse.py +1753 -0
- chrys/foundation/patches/textual_removed_node_caches.py +86 -0
- chrys/foundation/patches/textual_removed_screen_callbacks.py +55 -0
- chrys/foundation/patches/textual_selection_extract.py +48 -0
- chrys/foundation/patches/textual_strip_cycles.py +119 -0
- chrys/foundation/patches/textual_tab_selection.py +1611 -0
- chrys/foundation/patches/textual_timer_skip.py +70 -0
- chrys/foundation/patches/textual_utf8_decoder.py +106 -0
- chrys/foundation/patches/textual_win_sleep.py +167 -0
- chrys/foundation/patches/watchdog_fsevents.py +70 -0
- chrys/foundation/patches/watchdog_windows.py +52 -0
- chrys/foundation/platform/__init__.py +556 -0
- chrys/foundation/platform/c_api.py +32 -0
- chrys/foundation/platform/child_reap.py +107 -0
- chrys/foundation/platform/command_line.py +95 -0
- chrys/foundation/platform/files.py +1470 -0
- chrys/foundation/platform/output_capture.py +113 -0
- chrys/foundation/platform/paths.py +193 -0
- chrys/foundation/platform/process.py +1468 -0
- chrys/foundation/platform/pty_output.py +19 -0
- chrys/foundation/platform/runtime_paths.py +281 -0
- chrys/foundation/platform/win_console.py +129 -0
- chrys/foundation/reasoning_origin.py +67 -0
- chrys/foundation/recovery.py +16 -0
- chrys/foundation/retry.py +278 -0
- chrys/foundation/text/__init__.py +3 -0
- chrys/foundation/text/encoding.py +603 -0
- chrys/foundation/text/images.py +304 -0
- chrys/foundation/text/lines.py +24 -0
- chrys/foundation/text/mentions.py +90 -0
- chrys/foundation/text/model_json.py +25 -0
- chrys/foundation/text/reading_order.py +20 -0
- chrys/foundation/text/tokenizer.py +25 -0
- chrys/foundation/text/tool_output.py +287 -0
- chrys/foundation/text/yaml_io.py +62 -0
- chrys/foundation/tool_call_context.py +202 -0
- chrys/foundation/tool_execution_stamp.py +184 -0
- chrys/foundation/tool_invocation_order.py +48 -0
- chrys/foundation/tool_kinds.py +102 -0
- chrys/foundation/tool_result_metadata.py +287 -0
- chrys/foundation/trajectory/__init__.py +10 -0
- chrys/foundation/trajectory/context.py +481 -0
- chrys/foundation/trajectory/envelope.py +571 -0
- chrys/foundation/trajectory/event_types.py +294 -0
- chrys/foundation/trajectory/finalizers.py +46 -0
- chrys/foundation/trajectory/fingerprint.py +62 -0
- chrys/foundation/trajectory/ids.py +155 -0
- chrys/foundation/trajectory/keys.py +99 -0
- chrys/foundation/trajectory/lease.py +173 -0
- chrys/foundation/trajectory/metadata.py +72 -0
- chrys/foundation/trajectory/reader.py +271 -0
- chrys/foundation/trajectory/recovery.py +225 -0
- chrys/foundation/trajectory/revisions.py +152 -0
- chrys/foundation/trajectory/segments.py +225 -0
- chrys/foundation/trajectory/tools.py +117 -0
- chrys/foundation/trajectory/usage.py +151 -0
- chrys/foundation/trajectory/writer.py +972 -0
- chrys/foundation/trajectory_timing.py +97 -0
- chrys/foundation/util/__init__.py +3 -0
- chrys/foundation/util/async_tasks.py +29 -0
- chrys/foundation/util/chrys_headers.py +23 -0
- chrys/foundation/util/env_templates.py +104 -0
- chrys/foundation/util/filenames.py +96 -0
- chrys/foundation/util/header_charset.py +138 -0
- chrys/foundation/util/httpx_helpers.py +12 -0
- chrys/foundation/util/lock.py +236 -0
- chrys/foundation/util/once_close.py +46 -0
- chrys/foundation/util/session_ids.py +29 -0
- chrys/foundation/util/shell_tokens.py +88 -0
- chrys/foundation/util/sub_agent_context.py +38 -0
- chrys/foundation/util/time.py +35 -0
- chrys/foundation/util/unicode_scalars.py +36 -0
- chrys/foundation/vendor/__init__.py +43 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-unknown-linux-gnu +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-unknown-linux-musl +0 -0
- chrys/kernel/__init__.py +256 -0
- chrys/kernel/_content.py +1866 -0
- chrys/kernel/_loop_recorder.py +631 -0
- chrys/kernel/_null_overlay.py +1026 -0
- chrys/kernel/_result_ceiling.py +216 -0
- chrys/kernel/_serialization.py +431 -0
- chrys/kernel/_tool_arg_errors.py +1629 -0
- chrys/kernel/_tool_execution.py +1172 -0
- chrys/kernel/_tool_expansion.py +26 -0
- chrys/kernel/_types.py +2598 -0
- chrys/kernel/agent.py +994 -0
- chrys/kernel/client.py +1084 -0
- chrys/kernel/compaction.py +1315 -0
- chrys/kernel/exceptions.py +211 -0
- chrys/kernel/exchanges.py +683 -0
- chrys/kernel/identity.py +95 -0
- chrys/kernel/images.py +36 -0
- chrys/kernel/instrumentation.py +1618 -0
- chrys/kernel/loop.py +2308 -0
- chrys/kernel/middleware.py +616 -0
- chrys/kernel/sessions.py +604 -0
- chrys/kernel/tools.py +1061 -0
- chrys/kernel/types.py +126 -0
- chrys/orchestration/__init__.py +3 -0
- chrys/orchestration/engine/__init__.py +3 -0
- chrys/orchestration/engine/assembly.py +236 -0
- chrys/orchestration/engine/build/__init__.py +3 -0
- chrys/orchestration/engine/build/builder.py +1061 -0
- chrys/orchestration/engine/build/construction.py +408 -0
- chrys/orchestration/engine/build/loaded.py +139 -0
- chrys/orchestration/engine/engine.py +789 -0
- chrys/orchestration/engine/execution.py +771 -0
- chrys/orchestration/engine/loader.py +865 -0
- chrys/orchestration/engine/rollback.py +918 -0
- chrys/orchestration/engine/run/__init__.py +3 -0
- chrys/orchestration/engine/run/active_injection.py +419 -0
- chrys/orchestration/engine/run/attachments.py +519 -0
- chrys/orchestration/engine/run/bindings.py +1095 -0
- chrys/orchestration/engine/run/coordinator.py +1062 -0
- chrys/orchestration/engine/run/finalizer.py +417 -0
- chrys/orchestration/engine/run/input_refs.py +60 -0
- chrys/orchestration/engine/run/prompt_content.py +457 -0
- chrys/orchestration/engine/run/resume.py +299 -0
- chrys/orchestration/engine/run/retry.py +948 -0
- chrys/orchestration/engine/run/runner.py +625 -0
- chrys/orchestration/engine/run/runtime_skills.py +146 -0
- chrys/orchestration/engine/run/sub_agent_coordination.py +126 -0
- chrys/orchestration/engine/run/turn_hooks.py +258 -0
- chrys/orchestration/engine/run/turn_state.py +149 -0
- chrys/orchestration/engine/run/working_dir.py +50 -0
- chrys/orchestration/engine/session_lifecycle.py +2013 -0
- chrys/orchestration/engine/state/__init__.py +3 -0
- chrys/orchestration/engine/state/active_session.py +183 -0
- chrys/orchestration/engine/state/controls.py +639 -0
- chrys/orchestration/engine/state/current_agent.py +26 -0
- chrys/orchestration/engine/state/lifecycle_permits.py +395 -0
- chrys/orchestration/engine/state/machine.py +193 -0
- chrys/orchestration/engine/state/session_writer.py +434 -0
- chrys/orchestration/engine/trajectory.py +714 -0
- chrys/orchestration/engine/usage.py +118 -0
- chrys/orchestration/invoker/__init__.py +3 -0
- chrys/orchestration/invoker/acp.py +727 -0
- chrys/orchestration/invoker/acp_protocol.py +1115 -0
- chrys/orchestration/invoker/acp_spec.py +79 -0
- chrys/orchestration/invoker/attempts.py +994 -0
- chrys/orchestration/invoker/child_compaction.py +131 -0
- chrys/orchestration/invoker/child_history.py +99 -0
- chrys/orchestration/invoker/contracts.py +253 -0
- chrys/orchestration/invoker/evidence.py +94 -0
- chrys/orchestration/invoker/kernel.py +343 -0
- chrys/orchestration/invoker/origin.py +79 -0
- chrys/orchestration/invoker/resources.py +343 -0
- chrys/orchestration/invoker/runtime.py +299 -0
- chrys/orchestration/invoker/usage.py +80 -0
- chrys/orchestration/session_hooks.py +117 -0
- chrys/orchestration/session_host.py +1023 -0
- chrys/orchestration/session_resources.py +95 -0
- chrys/orchestration/session_usage.py +219 -0
- chrys/orchestration/startup.py +255 -0
- chrys/orchestration/sub_agents/__init__.py +3 -0
- chrys/orchestration/sub_agents/acp_policy.py +269 -0
- chrys/orchestration/sub_agents/kernel_policy.py +848 -0
- chrys/orchestration/sub_agents/registration.py +44 -0
- chrys/orchestration/sub_agents/shell.py +337 -0
- chrys/orchestration/sub_agents/tools.py +2228 -0
- chrys/orchestration/workflows/__init__.py +3 -0
- chrys/orchestration/workflows/agent_archive.py +156 -0
- chrys/orchestration/workflows/agent_node.py +894 -0
- chrys/orchestration/workflows/agent_node_build.py +687 -0
- chrys/orchestration/workflows/catalog.py +332 -0
- chrys/orchestration/workflows/coordinator.py +771 -0
- chrys/orchestration/workflows/hooks.py +176 -0
- chrys/orchestration/workflows/preview.py +322 -0
- chrys/orchestration/workflows/rollback.py +151 -0
- chrys/orchestration/workflows/runner.py +986 -0
- chrys/orchestration/workflows/session.py +390 -0
- chrys/orchestration/workflows/settings.py +97 -0
- chrys/orchestration/workflows/validation.py +674 -0
- chrys/orchestration/workflows/worker_client.py +1002 -0
- chrys/service/__init__.py +3 -0
- chrys/service/acp_client/__init__.py +65 -0
- chrys/service/acp_client/_transport.py +922 -0
- chrys/service/acp_client/client.py +1311 -0
- chrys/service/acp_client/errors.py +224 -0
- chrys/service/acp_client/protocol.py +220 -0
- chrys/service/acp_client/spawn.py +239 -0
- chrys/service/acp_client/spec.py +100 -0
- chrys/service/agent_middleware/__init__.py +60 -0
- chrys/service/agent_middleware/_metadata_keys.py +52 -0
- chrys/service/agent_middleware/control/__init__.py +3 -0
- chrys/service/agent_middleware/control/approval.py +769 -0
- chrys/service/agent_middleware/control/approval_hooks.py +62 -0
- chrys/service/agent_middleware/control/ask_user.py +207 -0
- chrys/service/agent_middleware/control/interrupt.py +50 -0
- chrys/service/agent_middleware/control/sleep.py +118 -0
- chrys/service/agent_middleware/control/todo.py +117 -0
- chrys/service/agent_middleware/events/__init__.py +3 -0
- chrys/service/agent_middleware/events/hook_dispatch.py +250 -0
- chrys/service/agent_middleware/events/hosted_tools.py +1762 -0
- chrys/service/agent_middleware/events/intermediate_text.py +128 -0
- chrys/service/agent_middleware/events/rejection_metadata.py +50 -0
- chrys/service/agent_middleware/events/result_persistence.py +98 -0
- chrys/service/agent_middleware/events/sub_agent_events.py +951 -0
- chrys/service/agent_middleware/events/tool_error_text.py +22 -0
- chrys/service/agent_middleware/events/tool_events.py +779 -0
- chrys/service/agent_middleware/injection.py +510 -0
- chrys/service/agent_middleware/reminders/__init__.py +91 -0
- chrys/service/agent_middleware/reminders/archive_pointer.py +154 -0
- chrys/service/agent_middleware/reminders/base.py +25 -0
- chrys/service/agent_middleware/reminders/context_usage.py +74 -0
- chrys/service/agent_middleware/reminders/file_change.py +63 -0
- chrys/service/agent_middleware/reminders/mcp.py +39 -0
- chrys/service/agent_middleware/reminders/profile_switch.py +106 -0
- chrys/service/agent_middleware/reminders/runtime_env.py +190 -0
- chrys/service/agent_middleware/reminders/skills.py +44 -0
- chrys/service/agent_middleware/reminders/sub_agents.py +35 -0
- chrys/service/agent_middleware/reminders/todo.py +35 -0
- chrys/service/agent_middleware/reminders/turn_line.py +48 -0
- chrys/service/agent_middleware/response_validation.py +1380 -0
- chrys/service/agent_middleware/system_reminder.py +1107 -0
- chrys/service/agent_middleware/validators.py +396 -0
- chrys/service/analytics/__init__.py +124 -0
- chrys/service/analytics/_actions.py +896 -0
- chrys/service/analytics/_context_evidence.py +357 -0
- chrys/service/analytics/_critical_path.py +179 -0
- chrys/service/analytics/_facts.py +919 -0
- chrys/service/analytics/_insights.py +769 -0
- chrys/service/analytics/_metric_ops.py +83 -0
- chrys/service/analytics/_session_projection.py +460 -0
- chrys/service/analytics/_timeline.py +434 -0
- chrys/service/analytics/_turn_graph.py +1237 -0
- chrys/service/analytics/_turns.py +1377 -0
- chrys/service/analytics/_workflows.py +171 -0
- chrys/service/analytics/aggregation.py +1474 -0
- chrys/service/analytics/classification.py +128 -0
- chrys/service/analytics/export.py +803 -0
- chrys/service/analytics/findings.py +282 -0
- chrys/service/analytics/math.py +79 -0
- chrys/service/analytics/model.py +886 -0
- chrys/service/analytics/reader.py +321 -0
- chrys/service/approval/__init__.py +3 -0
- chrys/service/approval/arbitration.py +102 -0
- chrys/service/approval/command_identity.py +109 -0
- chrys/service/approval/correlation.py +57 -0
- chrys/service/approval/grant_store.py +108 -0
- chrys/service/approval/judge.py +589 -0
- chrys/service/approval/level.py +15 -0
- chrys/service/approval/policy.py +209 -0
- chrys/service/approval/reuse.py +223 -0
- chrys/service/approval/reuse_binding.py +133 -0
- chrys/service/approval/safety_classifier.py +536 -0
- chrys/service/approval/turn_context.py +49 -0
- chrys/service/context/__init__.py +3 -0
- chrys/service/context/compaction/__init__.py +38 -0
- chrys/service/context/compaction/breaker.py +156 -0
- chrys/service/context/compaction/budgets.py +93 -0
- chrys/service/context/compaction/calibration.py +77 -0
- chrys/service/context/compaction/compression.py +690 -0
- chrys/service/context/compaction/current_turn_drop.py +458 -0
- chrys/service/context/compaction/events.py +201 -0
- chrys/service/context/compaction/exclusions.py +261 -0
- chrys/service/context/compaction/groups.py +361 -0
- chrys/service/context/compaction/last_words.py +1996 -0
- chrys/service/context/compaction/last_words_state.py +752 -0
- chrys/service/context/compaction/scoped.py +472 -0
- chrys/service/context/compaction/spill.py +1947 -0
- chrys/service/context/compaction/strategy.py +1116 -0
- chrys/service/context/compaction/summaries.py +319 -0
- chrys/service/context/compaction/turns.py +375 -0
- chrys/service/context/manager.py +174 -0
- chrys/service/context/memory_loader.py +792 -0
- chrys/service/context/middleware/__init__.py +3 -0
- chrys/service/context/middleware/usage.py +332 -0
- chrys/service/context/providers/__init__.py +3 -0
- chrys/service/context/providers/context_management.py +432 -0
- chrys/service/context/providers/history.py +647 -0
- chrys/service/context/providers/memory.py +50 -0
- chrys/service/context/tool_content.py +22 -0
- chrys/service/hooks/__init__.py +37 -0
- chrys/service/hooks/detached_worker.py +118 -0
- chrys/service/hooks/docs/README.md +14 -0
- chrys/service/hooks/docs/authoring.md +684 -0
- chrys/service/hooks/docs/configuration.md +499 -0
- chrys/service/hooks/docs/design.md +359 -0
- chrys/service/hooks/docs/project-hooks.md +348 -0
- chrys/service/hooks/events.py +157 -0
- chrys/service/hooks/loader.py +665 -0
- chrys/service/hooks/manager.py +807 -0
- chrys/service/hooks/matcher.py +111 -0
- chrys/service/hooks/outbox.py +294 -0
- chrys/service/hooks/runner.py +572 -0
- chrys/service/hooks/schema.py +407 -0
- chrys/service/llm/__init__.py +7 -0
- chrys/service/llm/_structured_outputs.py +192 -0
- chrys/service/llm/anthropic_messages/__init__.py +9 -0
- chrys/service/llm/anthropic_messages/client.py +254 -0
- chrys/service/llm/anthropic_messages/decode.py +331 -0
- chrys/service/llm/anthropic_messages/history.py +279 -0
- chrys/service/llm/anthropic_messages/request.py +323 -0
- chrys/service/llm/anthropic_messages/server_tools.py +319 -0
- chrys/service/llm/anthropic_messages/stream.py +297 -0
- chrys/service/llm/anthropic_messages/thinking_binding.py +156 -0
- chrys/service/llm/chat_completions/__init__.py +9 -0
- chrys/service/llm/chat_completions/client.py +369 -0
- chrys/service/llm/chat_completions/decode.py +338 -0
- chrys/service/llm/chat_completions/history.py +657 -0
- chrys/service/llm/chat_completions/reasoning.py +244 -0
- chrys/service/llm/chat_completions/request.py +192 -0
- chrys/service/llm/chat_completions/stream.py +546 -0
- chrys/service/llm/chat_completions/validation.py +308 -0
- chrys/service/llm/clients.py +613 -0
- chrys/service/llm/images.py +65 -0
- chrys/service/llm/mock.py +413 -0
- chrys/service/llm/observer.py +446 -0
- chrys/service/llm/one_shot.py +106 -0
- chrys/service/llm/openai_exceptions.py +93 -0
- chrys/service/llm/openai_responses/__init__.py +10 -0
- chrys/service/llm/openai_responses/client.py +323 -0
- chrys/service/llm/openai_responses/decode.py +432 -0
- chrys/service/llm/openai_responses/history.py +519 -0
- chrys/service/llm/openai_responses/hosted.py +625 -0
- chrys/service/llm/openai_responses/replay.py +629 -0
- chrys/service/llm/openai_responses/request.py +342 -0
- chrys/service/llm/openai_responses/stream.py +847 -0
- chrys/service/llm/openai_timestamps.py +57 -0
- chrys/service/llm/providers.py +107 -0
- chrys/service/llm/proxy_route.py +54 -0
- chrys/service/llm/raw_http_log.py +309 -0
- chrys/service/llm/route_facts.py +63 -0
- chrys/service/llm/route_sessions.py +49 -0
- chrys/service/llm/sdk_anthropic.py +18 -0
- chrys/service/llm/sdk_headers.py +47 -0
- chrys/service/llm/sdk_openai.py +18 -0
- chrys/service/llm/sdk_retry.py +59 -0
- chrys/service/llm/wire_client.py +246 -0
- chrys/service/mcp/__init__.py +3 -0
- chrys/service/mcp/_connection.py +235 -0
- chrys/service/mcp/_http_transport.py +350 -0
- chrys/service/mcp/_progressive.py +459 -0
- chrys/service/mcp/_stdio_transport.py +614 -0
- chrys/service/mcp/_tool_mixins.py +96 -0
- chrys/service/mcp/adapter.py +1031 -0
- chrys/service/mcp/cache.py +476 -0
- chrys/service/mcp/content.py +32 -0
- chrys/service/mcp/errors.py +169 -0
- chrys/service/mcp/owned.py +1330 -0
- chrys/service/mcp/result_limits.py +168 -0
- chrys/service/mcp/thinking_warning.py +95 -0
- chrys/service/mcp/validation.py +74 -0
- chrys/service/mutations/__init__.py +84 -0
- chrys/service/mutations/coordination.py +894 -0
- chrys/service/mutations/detector.py +458 -0
- chrys/service/mutations/git_calibrator.py +397 -0
- chrys/service/mutations/git_state.py +647 -0
- chrys/service/mutations/pipeline.py +779 -0
- chrys/service/mutations/scanner.py +466 -0
- chrys/service/mutations/snapshot_files.py +233 -0
- chrys/service/mutations/store.py +625 -0
- chrys/service/mutations/tool_names.py +15 -0
- chrys/service/mutations/trace.py +246 -0
- chrys/service/mutations/tracker.py +1289 -0
- chrys/service/mutations/types.py +559 -0
- chrys/service/mutations/workspace_changes.py +1583 -0
- chrys/service/profiles/__init__.py +2 -0
- chrys/service/profiles/agents/__init__.py +3 -0
- chrys/service/profiles/agents/builtins/Code.yaml +199 -0
- chrys/service/profiles/agents/builtins/Explore.yaml +62 -0
- chrys/service/profiles/agents/builtins/General.yaml +114 -0
- chrys/service/profiles/agents/builtins/QA.yaml +128 -0
- chrys/service/profiles/agents/loader.py +808 -0
- chrys/service/profiles/agents/registry.py +727 -0
- chrys/service/profiles/agents/schema.py +371 -0
- chrys/service/profiles/agents/serializer.py +196 -0
- chrys/service/profiles/coercion.py +22 -0
- chrys/service/profiles/models/__init__.py +3 -0
- chrys/service/profiles/models/env_bridge.py +157 -0
- chrys/service/profiles/models/loader.py +282 -0
- chrys/service/profiles/models/options.py +438 -0
- chrys/service/profiles/models/registry.py +104 -0
- chrys/service/profiles/models/resolver.py +286 -0
- chrys/service/profiles/models/schema.py +91 -0
- chrys/service/profiles/models/serializer.py +76 -0
- chrys/service/session/__init__.py +3 -0
- chrys/service/session/checkpoint.py +156 -0
- chrys/service/session/history.py +1616 -0
- chrys/service/session/message_metadata.py +151 -0
- chrys/service/session/persistence.py +435 -0
- chrys/service/session/runtime_metadata.py +210 -0
- chrys/service/session/sub_agent_logs.py +1069 -0
- chrys/service/session/sub_agent_transcript.py +654 -0
- chrys/service/skills/__init__.py +3 -0
- chrys/service/skills/adapter.py +408 -0
- chrys/service/skills/constants.py +8 -0
- chrys/service/skills/loader.py +575 -0
- chrys/service/skills/model.py +130 -0
- chrys/service/skills/provider.py +841 -0
- chrys/service/skills/runner.py +390 -0
- chrys/service/state/__init__.py +3 -0
- chrys/service/state/_fork.py +710 -0
- chrys/service/state/_session_files.py +175 -0
- chrys/service/state/_session_meta.py +552 -0
- chrys/service/state/locks.py +154 -0
- chrys/service/state/serializers.py +156 -0
- chrys/service/state/session_catalog.py +361 -0
- chrys/service/state/session_listing.py +84 -0
- chrys/service/state/session_migration.py +446 -0
- chrys/service/state/session_mru.py +315 -0
- chrys/service/state/store.py +2132 -0
- chrys/service/state/workflow.py +158 -0
- chrys/service/text_blocks.py +47 -0
- chrys/service/todos/__init__.py +3 -0
- chrys/service/todos/reminder.py +31 -0
- chrys/service/todos/tracker.py +58 -0
- chrys/service/tools/__init__.py +3 -0
- chrys/service/tools/approval_targets.py +92 -0
- chrys/service/tools/builtins/__init__.py +3 -0
- chrys/service/tools/builtins/ask_user.py +326 -0
- chrys/service/tools/builtins/doc_converter/__init__.py +494 -0
- chrys/service/tools/builtins/doc_converter/artifacts.py +392 -0
- chrys/service/tools/builtins/doc_converter/parsers/__init__.py +3 -0
- chrys/service/tools/builtins/doc_converter/parsers/_table.py +34 -0
- chrys/service/tools/builtins/doc_converter/parsers/base.py +101 -0
- chrys/service/tools/builtins/doc_converter/parsers/docx.py +88 -0
- chrys/service/tools/builtins/doc_converter/parsers/pdf.py +144 -0
- chrys/service/tools/builtins/doc_converter/parsers/pptx.py +103 -0
- chrys/service/tools/builtins/doc_converter/parsers/xls.py +32 -0
- chrys/service/tools/builtins/doc_converter/parsers/xlsx.py +51 -0
- chrys/service/tools/builtins/doc_converter/registry.py +55 -0
- chrys/service/tools/builtins/filesystem.py +1071 -0
- chrys/service/tools/builtins/search.py +1265 -0
- chrys/service/tools/builtins/shell.py +967 -0
- chrys/service/tools/builtins/shell_command_filtering/__init__.py +5 -0
- chrys/service/tools/builtins/shell_command_filtering/policy.py +60 -0
- chrys/service/tools/builtins/shell_command_filtering/presets.py +152 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/__init__.py +358 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/commands.py +416 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/git.py +408 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/syntax.py +87 -0
- chrys/service/tools/builtins/shell_command_filtering/tokenizer.py +214 -0
- chrys/service/tools/builtins/shell_filter.py +24 -0
- chrys/service/tools/builtins/sleep.py +91 -0
- chrys/service/tools/builtins/todo.py +64 -0
- chrys/service/tools/builtins/web/__init__.py +6 -0
- chrys/service/tools/builtins/web/build.py +268 -0
- chrys/service/tools/builtins/web/config.py +338 -0
- chrys/service/tools/builtins/web/credentials.py +37 -0
- chrys/service/tools/builtins/web/fetch/__init__.py +3 -0
- chrys/service/tools/builtins/web/fetch/cache.py +68 -0
- chrys/service/tools/builtins/web/fetch/convert.py +56 -0
- chrys/service/tools/builtins/web/fetch/tool.py +223 -0
- chrys/service/tools/builtins/web/hosted.py +61 -0
- chrys/service/tools/builtins/web/http.py +330 -0
- chrys/service/tools/builtins/web/progress.py +18 -0
- chrys/service/tools/builtins/web/search/__init__.py +3 -0
- chrys/service/tools/builtins/web/search/exa_mcp.py +146 -0
- chrys/service/tools/builtins/web/search/html.py +152 -0
- chrys/service/tools/builtins/web/search/json_api.py +227 -0
- chrys/service/tools/builtins/web/search/providers.py +77 -0
- chrys/service/tools/builtins/web/search/tool.py +218 -0
- chrys/service/tools/builtins/web/search/types.py +28 -0
- chrys/service/tools/builtins/web/tls_identity.py +66 -0
- chrys/service/tools/kinds.py +115 -0
- chrys/service/tools/names.py +47 -0
- chrys/service/tools/registry.py +242 -0
- chrys/service/tools/result_metadata.py +103 -0
- chrys/service/tools/session_artifacts.py +196 -0
- chrys/service/tools/spill.py +218 -0
- chrys/service/tools/workspace_paths.py +81 -0
- chrys/service/trajectory/__init__.py +11 -0
- chrys/service/trajectory/approvals.py +201 -0
- chrys/service/trajectory/compaction.py +420 -0
- chrys/service/trajectory/fork.py +75 -0
- chrys/service/trajectory/hooks.py +217 -0
- chrys/service/trajectory/items.py +41 -0
- chrys/service/trajectory/preparation.py +374 -0
- chrys/service/trajectory/retries.py +95 -0
- chrys/service/trajectory/revisions.py +129 -0
- chrys/service/trajectory/segmented.py +163 -0
- chrys/service/trajectory/session.py +762 -0
- chrys/service/trajectory/state.py +83 -0
- chrys/service/trajectory/sub_agents.py +169 -0
- chrys/service/trajectory/tombstone.py +262 -0
- chrys/service/trajectory/tools.py +323 -0
- chrys/service/trajectory/validation.py +130 -0
- chrys/service/trajectory/waits.py +161 -0
- chrys/service/trajectory/workflow.py +148 -0
- chrys/service/vision.py +128 -0
- chrys/service/workflows/__init__.py +3 -0
- chrys/service/workflows/admission.py +151 -0
- chrys/service/workflows/artifacts.py +109 -0
- chrys/service/workflows/asks.py +130 -0
- chrys/service/workflows/builtins/__init__.py +12 -0
- chrys/service/workflows/builtins/demo-workflow.manifest.json +635 -0
- chrys/service/workflows/builtins/demo-workflow.py +756 -0
- chrys/service/workflows/discovery.py +554 -0
- chrys/service/workflows/environment.py +284 -0
- chrys/service/workflows/graph.py +430 -0
- chrys/service/workflows/history.py +82 -0
- chrys/service/workflows/interpreter.py +107 -0
- chrys/service/workflows/journal.py +334 -0
- chrys/service/workflows/layout.py +56 -0
- chrys/service/workflows/ledger.py +149 -0
- chrys/service/workflows/model_selection.py +18 -0
- chrys/service/workflows/orphans.py +209 -0
- chrys/service/workflows/outcomes.py +46 -0
- chrys/service/workflows/protocol.py +210 -0
- chrys/service/workflows/records.py +142 -0
- chrys/service/workflows/scheduler.py +770 -0
- chrys/service/workflows/sdk/__init__.py +51 -0
- chrys/service/workflows/sdk/_ask.py +167 -0
- chrys/service/workflows/sdk/_builder.py +781 -0
- chrys/service/workflows/sdk/_values.py +92 -0
- chrys/service/workflows/sdk_artifact.py +74 -0
- chrys/service/workflows/store.py +494 -0
- chrys/service/workflows/transcript.py +102 -0
- chrys/service/workflows/values.py +73 -0
- chrys/service/workflows/worker_host.py +1820 -0
- chrys/workflows.py +38 -0
- icode_tui-0.29.1.dist-info/METADATA +104 -0
- icode_tui-0.29.1.dist-info/RECORD +1138 -0
- icode_tui-0.29.1.dist-info/WHEEL +4 -0
- icode_tui-0.29.1.dist-info/entry_points.txt +2 -0
- icode_tui-0.29.1.dist-info/licenses/LICENSE +202 -0
- icode_tui-0.29.1.dist-info/licenses/NOTICE +1955 -0
|
@@ -0,0 +1,1605 @@
|
|
|
1
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Replay planning for persisted chat transcript history."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from contextlib import suppress
|
|
10
|
+
from dataclasses import dataclass, field, replace
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from chrys.app.tui.widgets.chat.context_fold import ContextFoldWidget
|
|
14
|
+
from chrys.app.tui.widgets.chat.messages import (
|
|
15
|
+
INTERRUPTED_CONTINUE_ACTION_MESSAGE,
|
|
16
|
+
INTERRUPTED_RETRY_ACTION_MESSAGE,
|
|
17
|
+
AgentMessage,
|
|
18
|
+
InterruptedMessage,
|
|
19
|
+
UserMessage,
|
|
20
|
+
format_message_created_at,
|
|
21
|
+
process_think_tags,
|
|
22
|
+
resolve_interrupted_message_copy,
|
|
23
|
+
)
|
|
24
|
+
from chrys.app.tui.widgets.chat.ports import ChatMountPort, TranscriptLocalizationPort, TranscriptUiPort
|
|
25
|
+
from chrys.app.tui.widgets.chat.toc_model import TurnTocModel, turn_index_from_extra
|
|
26
|
+
from chrys.app.tui.widgets.chat.tool_call import ToolGroup
|
|
27
|
+
from chrys.foundation.hosted_tools import (
|
|
28
|
+
HOSTED_TOOL_DEFAULT_KIND_BY_FAMILY,
|
|
29
|
+
HostedToolFamily,
|
|
30
|
+
)
|
|
31
|
+
from chrys.foundation.i18n import MessageDef, MessageRef
|
|
32
|
+
from chrys.foundation.i18n.formatting import format_message, sanitize_legacy_block
|
|
33
|
+
from chrys.foundation.models.history_markers import (
|
|
34
|
+
AWAITING_SUB_AGENTS_MESSAGE,
|
|
35
|
+
EXECUTION_FAILED_MESSAGE,
|
|
36
|
+
EXECUTION_INTERRUPTED_MESSAGE,
|
|
37
|
+
SESSION_CLOSED_MESSAGE,
|
|
38
|
+
SUB_AGENT_STATE_DISCARDED_MESSAGE,
|
|
39
|
+
HistoryMarkerKind,
|
|
40
|
+
)
|
|
41
|
+
from chrys.foundation.text.images import is_image_media_type
|
|
42
|
+
from chrys.foundation.tool_result_metadata import TOOL_INTERRUPTED_METADATA_KEY
|
|
43
|
+
from chrys.foundation.trajectory_timing import trajectory_timing_from_metadata
|
|
44
|
+
from chrys.foundation.util.time import parse_created_at
|
|
45
|
+
from chrys.kernel import Content, Message
|
|
46
|
+
from chrys.kernel.exchanges import (
|
|
47
|
+
LEGACY_CALL_CONTENT_TYPE,
|
|
48
|
+
TOOL_CALL_CONTENT_TYPES,
|
|
49
|
+
TOOL_RESULT_CONTENT_TYPES,
|
|
50
|
+
DictAccessor,
|
|
51
|
+
EmptyIdPolicy,
|
|
52
|
+
NoneIdPolicy,
|
|
53
|
+
PairingPolicy,
|
|
54
|
+
iter_exchanges,
|
|
55
|
+
pair_results,
|
|
56
|
+
)
|
|
57
|
+
from chrys.service.agent_middleware.events.hosted_tools import (
|
|
58
|
+
HostedToolView,
|
|
59
|
+
ResponsePresentationPlan,
|
|
60
|
+
_is_hosted_call,
|
|
61
|
+
_is_hosted_result,
|
|
62
|
+
adapt_hosted_tool,
|
|
63
|
+
hosted_replay_status,
|
|
64
|
+
is_legacy_server_executed_informational_call,
|
|
65
|
+
normalize_hosted_replay_content,
|
|
66
|
+
)
|
|
67
|
+
from chrys.service.agent_middleware.events.result_persistence import PERSISTED_RESULT_METADATA_KEYS
|
|
68
|
+
from chrys.service.mutations.tool_names import _FILE_TOOLS
|
|
69
|
+
from chrys.service.session.message_metadata import (
|
|
70
|
+
MESSAGE_CREATED_AT_KEY,
|
|
71
|
+
TOOL_RESULT_METADATA_KEY,
|
|
72
|
+
is_compaction_tool_summary,
|
|
73
|
+
persisted_tool_call_kind,
|
|
74
|
+
)
|
|
75
|
+
from chrys.service.text_blocks import join_text_blocks, reconstruct_text_blocks, text_block_id
|
|
76
|
+
|
|
77
|
+
if TYPE_CHECKING:
|
|
78
|
+
from textual.widget import Widget
|
|
79
|
+
|
|
80
|
+
from chrys.service.context.providers.history import CompressedBlock
|
|
81
|
+
|
|
82
|
+
# Replay renders every function-style call it can display: legacy serialized calls
|
|
83
|
+
# count as calls, informational calls still get cards, and idless calls/results pair
|
|
84
|
+
# positionally within their exchange (None-id and empty-id queues kept separate).
|
|
85
|
+
# Non-string ids are stringified so lookups match the str-normalized `_call_id` view.
|
|
86
|
+
_REPLAY_PAIRING_POLICY = PairingPolicy(
|
|
87
|
+
call_types=TOOL_CALL_CONTENT_TYPES | {LEGACY_CALL_CONTENT_TYPE},
|
|
88
|
+
include_informational_calls=True,
|
|
89
|
+
result_types=TOOL_RESULT_CONTENT_TYPES,
|
|
90
|
+
none_id=NoneIdPolicy.POSITIONAL,
|
|
91
|
+
empty_id=EmptyIdPolicy.POSITIONAL,
|
|
92
|
+
malformed_id="stringify",
|
|
93
|
+
)
|
|
94
|
+
_REPLAY_ONLY_CONTENT_KEYS = {
|
|
95
|
+
"_file_snapshot",
|
|
96
|
+
"_replay_approval",
|
|
97
|
+
"_replay_artifacts",
|
|
98
|
+
"_replay_image_contents",
|
|
99
|
+
"_replay_metadata",
|
|
100
|
+
"_replay_result",
|
|
101
|
+
}
|
|
102
|
+
_STATUS_MESSAGE_DEFINITIONS: dict[str, MessageDef] = {
|
|
103
|
+
HistoryMarkerKind.STATUS_EXECUTION_INTERRUPTED: EXECUTION_INTERRUPTED_MESSAGE,
|
|
104
|
+
HistoryMarkerKind.STATUS_EXECUTION_FAILED: EXECUTION_FAILED_MESSAGE,
|
|
105
|
+
HistoryMarkerKind.STATUS_SESSION_CLOSED: SESSION_CLOSED_MESSAGE,
|
|
106
|
+
HistoryMarkerKind.STATUS_SUB_AGENT_STATE_DISCARDED: SUB_AGENT_STATE_DISCARDED_MESSAGE,
|
|
107
|
+
HistoryMarkerKind.STATUS_AWAITING_SUB_AGENTS: AWAITING_SUB_AGENTS_MESSAGE,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True)
|
|
112
|
+
class ReplayCompatibilityAnnotation:
|
|
113
|
+
"""Legacy mutation that must still be applied to the caller's raw message dicts."""
|
|
114
|
+
|
|
115
|
+
target: dict[str, Any]
|
|
116
|
+
values: dict[str, Any]
|
|
117
|
+
|
|
118
|
+
def apply(self) -> None:
|
|
119
|
+
self.target.update(self.values)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class ReplayToolCall:
|
|
124
|
+
"""A planned tool invocation with locally paired replay data."""
|
|
125
|
+
|
|
126
|
+
source: dict[str, Any]
|
|
127
|
+
name: str
|
|
128
|
+
call_id: str
|
|
129
|
+
tool_kind: str = ""
|
|
130
|
+
arguments: Any = None
|
|
131
|
+
result: Any = ""
|
|
132
|
+
metadata: dict[str, Any] | None = None
|
|
133
|
+
image_contents: list[dict[str, Any]] | None = None
|
|
134
|
+
file_snapshot: Any = None
|
|
135
|
+
approval: dict[str, Any] | None = None
|
|
136
|
+
provider_hosted: bool = False
|
|
137
|
+
hosted_family: str = ""
|
|
138
|
+
provider: str = ""
|
|
139
|
+
provider_item_type: str = ""
|
|
140
|
+
provider_status: str = ""
|
|
141
|
+
provider_call_id: str = ""
|
|
142
|
+
canonical_status: str = "completed"
|
|
143
|
+
artifacts: list[dict[str, Any]] | None = None
|
|
144
|
+
duration_ms: int | None = None
|
|
145
|
+
started_at: str | None = None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass(frozen=True)
|
|
149
|
+
class ReplayTurnMarker:
|
|
150
|
+
turn_index: int | None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(frozen=True)
|
|
154
|
+
class ReplayUserMessage:
|
|
155
|
+
text: str
|
|
156
|
+
contents: list[Any]
|
|
157
|
+
created_at: Any = None
|
|
158
|
+
is_injection: bool = False
|
|
159
|
+
profile_switch_to: str = ""
|
|
160
|
+
duration_ms: int | None = None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(frozen=True)
|
|
164
|
+
class ReplayProfileSwitch:
|
|
165
|
+
profile_name: str
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass(frozen=True)
|
|
169
|
+
class ReplayAgentMessage:
|
|
170
|
+
contents: list[Any]
|
|
171
|
+
created_at: Any = None
|
|
172
|
+
is_intermediate: bool = False
|
|
173
|
+
structured_output_completed: bool = False
|
|
174
|
+
intermediate_texts: list[str] = field(default_factory=list)
|
|
175
|
+
status_code: str | None = None
|
|
176
|
+
status_count: int | None = None
|
|
177
|
+
duration_ms: int | None = None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass(frozen=True)
|
|
181
|
+
class ReplayContextFold:
|
|
182
|
+
context_id: str
|
|
183
|
+
summary: str
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclass(frozen=True)
|
|
187
|
+
class ReplayInterrupted:
|
|
188
|
+
reason: str
|
|
189
|
+
source: str = "user"
|
|
190
|
+
action_label: MessageDef | None = None
|
|
191
|
+
status_code: str | None = None
|
|
192
|
+
status_count: int | None = None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
ReplayEntry = (
|
|
196
|
+
ReplayTurnMarker
|
|
197
|
+
| ReplayUserMessage
|
|
198
|
+
| ReplayProfileSwitch
|
|
199
|
+
| ReplayAgentMessage
|
|
200
|
+
| ReplayContextFold
|
|
201
|
+
| ReplayInterrupted
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass(frozen=True)
|
|
206
|
+
class ReplayPlan:
|
|
207
|
+
"""Ordered replay operations plus compatibility annotations."""
|
|
208
|
+
|
|
209
|
+
entries: list[ReplayEntry]
|
|
210
|
+
annotations: list[ReplayCompatibilityAnnotation]
|
|
211
|
+
|
|
212
|
+
def apply_compatibility_annotations(self) -> None:
|
|
213
|
+
for annotation in self.annotations:
|
|
214
|
+
annotation.apply()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@dataclass
|
|
218
|
+
class _PairedToolData:
|
|
219
|
+
result: Any = ""
|
|
220
|
+
metadata: dict[str, Any] | None = None
|
|
221
|
+
image_contents: list[dict[str, Any]] | None = None
|
|
222
|
+
file_snapshot: Any = None
|
|
223
|
+
hosted_view: HostedToolView | None = None
|
|
224
|
+
artifacts: list[dict[str, Any]] | None = None
|
|
225
|
+
has_result: bool = False
|
|
226
|
+
duration_ms: int | None = None
|
|
227
|
+
started_at: str | None = None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@dataclass
|
|
231
|
+
class _MergedAssistantEntry:
|
|
232
|
+
contents: list[Any]
|
|
233
|
+
additional_properties: dict[str, Any]
|
|
234
|
+
batch_id: Any = None
|
|
235
|
+
is_intermediate: bool = False
|
|
236
|
+
intermediate_texts: list[str] = field(default_factory=list)
|
|
237
|
+
has_preceding_text: bool = False
|
|
238
|
+
merged_role: str = ""
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@dataclass
|
|
242
|
+
class _RawEntry:
|
|
243
|
+
message: dict[str, Any]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
MergedEntry = _MergedAssistantEntry | _RawEntry
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class HistoryReplayPlanner:
|
|
250
|
+
"""Transform persisted raw messages into replay operations without mounting widgets."""
|
|
251
|
+
|
|
252
|
+
def build_plan(
|
|
253
|
+
self,
|
|
254
|
+
messages: list[dict[str, Any]],
|
|
255
|
+
*,
|
|
256
|
+
file_snapshots: dict[str, list[Any]] | None = None,
|
|
257
|
+
suppress_marker_text: bool = False,
|
|
258
|
+
) -> ReplayPlan:
|
|
259
|
+
# Compaction tool summaries are the model's context and never render.
|
|
260
|
+
# Drop them before pairing and merging, so a hidden summary cannot
|
|
261
|
+
# split or join the tool groups around it.
|
|
262
|
+
messages = [message for message in messages if not is_compaction_tool_summary(message)]
|
|
263
|
+
pairings, standalone_hosted_results, annotations = self._pair_tool_calls(
|
|
264
|
+
messages,
|
|
265
|
+
file_snapshots=file_snapshots,
|
|
266
|
+
)
|
|
267
|
+
merged = self._merge_assistant_entries(messages, pairings, standalone_hosted_results, annotations)
|
|
268
|
+
entries = self._build_entries(merged, suppress_marker_text=suppress_marker_text)
|
|
269
|
+
return ReplayPlan(entries=entries, annotations=annotations)
|
|
270
|
+
|
|
271
|
+
def _pair_tool_calls(
|
|
272
|
+
self,
|
|
273
|
+
messages: list[dict[str, Any]],
|
|
274
|
+
*,
|
|
275
|
+
file_snapshots: dict[str, list[Any]] | None,
|
|
276
|
+
) -> tuple[dict[int, _PairedToolData], dict[int, ReplayToolCall], list[ReplayCompatibilityAnnotation]]:
|
|
277
|
+
pairings: dict[int, _PairedToolData] = {}
|
|
278
|
+
standalone_result_coordinates: set[tuple[int, int]] = set()
|
|
279
|
+
annotations: list[ReplayCompatibilityAnnotation] = []
|
|
280
|
+
snapshot_cursors: dict[str, int] = {}
|
|
281
|
+
|
|
282
|
+
# Exchange-scoped pairing over the serialized transcript: every
|
|
283
|
+
# response sibling shares the exchange's output block, embedded
|
|
284
|
+
# results answer their own message's calls, markers are hard
|
|
285
|
+
# boundaries, and duplicate ids consume one shared per-key cursor.
|
|
286
|
+
accessor = DictAccessor()
|
|
287
|
+
result_by_call: dict[tuple[int, int], dict[str, Any] | None] = {}
|
|
288
|
+
paired_result_coordinates: set[tuple[int, int]] = set()
|
|
289
|
+
for exchange in iter_exchanges(messages, accessor):
|
|
290
|
+
pairing = pair_results(messages, exchange, accessor, _REPLAY_PAIRING_POLICY)
|
|
291
|
+
for slots in (*pairing.truthy_assignments.values(), *pairing.falsy_assignments.values()):
|
|
292
|
+
for call, result in slots:
|
|
293
|
+
result_content: dict[str, Any] | None = None
|
|
294
|
+
if result is not None:
|
|
295
|
+
paired_result_coordinates.add((result.message_index, result.content_index))
|
|
296
|
+
candidate = _message_contents(messages[result.message_index])[result.content_index]
|
|
297
|
+
result_content = candidate if isinstance(candidate, dict) else None
|
|
298
|
+
result_by_call[(call.message_index, call.content_index)] = result_content
|
|
299
|
+
unconsumed_results = [
|
|
300
|
+
result
|
|
301
|
+
for results in (*pairing.unconsumed_results.values(), *pairing.falsy_unconsumed_results.values())
|
|
302
|
+
for result in results
|
|
303
|
+
]
|
|
304
|
+
for result in [*unconsumed_results, *pairing.unpairable_results]:
|
|
305
|
+
coordinate = (result.message_index, result.content_index)
|
|
306
|
+
candidate = _message_contents(messages[result.message_index])[result.content_index]
|
|
307
|
+
if isinstance(candidate, dict) and _is_hosted_tool_result(candidate):
|
|
308
|
+
standalone_result_coordinates.add(coordinate)
|
|
309
|
+
|
|
310
|
+
# ``iter_exchanges`` deliberately starts only at a call-bearing
|
|
311
|
+
# assistant run, so a completely standalone output belongs to no
|
|
312
|
+
# exchange. Pairing remains exchange-scoped above; this pass only
|
|
313
|
+
# inventories canonical hosted result occurrences that the pairing
|
|
314
|
+
# authority did not consume, without attempting any id matching.
|
|
315
|
+
for message_index, message in enumerate(messages):
|
|
316
|
+
if message.get("role", "") not in {"assistant", "tool"}:
|
|
317
|
+
continue
|
|
318
|
+
if HistoryMarkerKind.KEY in _message_extra(message):
|
|
319
|
+
continue
|
|
320
|
+
for content_index, content in enumerate(_message_contents(message)):
|
|
321
|
+
coordinate = (message_index, content_index)
|
|
322
|
+
if (
|
|
323
|
+
coordinate not in paired_result_coordinates
|
|
324
|
+
and isinstance(content, dict)
|
|
325
|
+
and _is_hosted_tool_result(content)
|
|
326
|
+
):
|
|
327
|
+
standalone_result_coordinates.add(coordinate)
|
|
328
|
+
|
|
329
|
+
for index, msg in enumerate(messages):
|
|
330
|
+
if msg.get("role", "") != "assistant":
|
|
331
|
+
continue
|
|
332
|
+
if HistoryMarkerKind.KEY in _message_extra(msg):
|
|
333
|
+
# Marker payloads are chrome: they never render as tools, so
|
|
334
|
+
# they must not consume snapshot cursors that belong to the
|
|
335
|
+
# visible calls around them.
|
|
336
|
+
continue
|
|
337
|
+
indexed_tool_calls = [
|
|
338
|
+
(content_index, content)
|
|
339
|
+
for content_index, content in enumerate(_message_contents(msg))
|
|
340
|
+
if _is_tool_call(content)
|
|
341
|
+
]
|
|
342
|
+
if not indexed_tool_calls:
|
|
343
|
+
continue
|
|
344
|
+
|
|
345
|
+
for content_index, tool_call in indexed_tool_calls:
|
|
346
|
+
call_id = _call_id(tool_call)
|
|
347
|
+
result_content = result_by_call.get((index, content_index))
|
|
348
|
+
|
|
349
|
+
paired = _PairedToolData()
|
|
350
|
+
timing = _content_timing(result_content) or _content_timing(tool_call)
|
|
351
|
+
if timing is not None:
|
|
352
|
+
paired.duration_ms = timing["duration_ms"]
|
|
353
|
+
paired.started_at = timing["started_at"]
|
|
354
|
+
values: dict[str, Any] = {}
|
|
355
|
+
if _is_hosted_tool_call(tool_call):
|
|
356
|
+
call = _hosted_content_for_replay(tool_call)
|
|
357
|
+
result = _content_for_replay(result_content) if isinstance(result_content, dict) else None
|
|
358
|
+
paired.has_result = result is not None
|
|
359
|
+
paired.hosted_view = adapt_hosted_tool(call, result)
|
|
360
|
+
paired.result = paired.hosted_view.result_text
|
|
361
|
+
persisted_metadata = _replay_tool_metadata(result_content) if result_content is not None else None
|
|
362
|
+
paired.metadata = {
|
|
363
|
+
**(persisted_metadata or {}),
|
|
364
|
+
**paired.hosted_view.metadata,
|
|
365
|
+
} or None
|
|
366
|
+
paired.image_contents = [content.to_dict() for content in paired.hosted_view.image_contents] or None
|
|
367
|
+
paired.artifacts = _hosted_artifact_descriptors(paired.hosted_view) or None
|
|
368
|
+
elif isinstance(result_content, dict):
|
|
369
|
+
paired.result = result_content.get("result", "")
|
|
370
|
+
paired.metadata = _replay_tool_metadata(result_content)
|
|
371
|
+
images = _replay_tool_images(result_content)
|
|
372
|
+
paired.image_contents = images or None
|
|
373
|
+
values["_replay_result"] = paired.result
|
|
374
|
+
if paired.metadata is not None:
|
|
375
|
+
values["_replay_metadata"] = paired.metadata
|
|
376
|
+
if paired.image_contents:
|
|
377
|
+
values["_replay_image_contents"] = paired.image_contents
|
|
378
|
+
if paired.artifacts:
|
|
379
|
+
values["_replay_artifacts"] = paired.artifacts
|
|
380
|
+
|
|
381
|
+
if tool_call.get("name", "") in _FILE_TOOLS:
|
|
382
|
+
snapshots = file_snapshots.get(call_id, []) if file_snapshots else []
|
|
383
|
+
snapshot_index = snapshot_cursors.get(call_id, 0)
|
|
384
|
+
if snapshot_index < len(snapshots):
|
|
385
|
+
paired.file_snapshot = snapshots[snapshot_index]
|
|
386
|
+
values["_file_snapshot"] = paired.file_snapshot
|
|
387
|
+
snapshot_cursors[call_id] = snapshot_index + 1
|
|
388
|
+
|
|
389
|
+
pairings[id(tool_call)] = paired
|
|
390
|
+
annotations.append(ReplayCompatibilityAnnotation(tool_call, values))
|
|
391
|
+
|
|
392
|
+
standalone_hosted_results: dict[int, ReplayToolCall] = {}
|
|
393
|
+
for message_index, message in enumerate(messages):
|
|
394
|
+
for content_index, content in enumerate(_message_contents(message)):
|
|
395
|
+
if (message_index, content_index) not in standalone_result_coordinates:
|
|
396
|
+
continue
|
|
397
|
+
standalone_hosted_results[id(content)] = self._standalone_hosted_replay_tool(
|
|
398
|
+
content,
|
|
399
|
+
message_index=message_index,
|
|
400
|
+
content_index=content_index,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
return pairings, standalone_hosted_results, annotations
|
|
404
|
+
|
|
405
|
+
def _merge_assistant_entries(
|
|
406
|
+
self,
|
|
407
|
+
messages: list[dict[str, Any]],
|
|
408
|
+
pairings: dict[int, _PairedToolData],
|
|
409
|
+
standalone_hosted_results: dict[int, ReplayToolCall],
|
|
410
|
+
annotations: list[ReplayCompatibilityAnnotation],
|
|
411
|
+
) -> list[MergedEntry]:
|
|
412
|
+
merged: list[MergedEntry] = []
|
|
413
|
+
legacy_duplicate_sidecars = _legacy_duplicate_intermediate_sidecars(messages)
|
|
414
|
+
for message_index, msg in enumerate(messages):
|
|
415
|
+
role = msg.get("role", "")
|
|
416
|
+
if role not in {"assistant", "tool"}:
|
|
417
|
+
merged.append(_RawEntry(msg))
|
|
418
|
+
continue
|
|
419
|
+
|
|
420
|
+
contents = _message_contents(msg)
|
|
421
|
+
extra = _message_extra(msg)
|
|
422
|
+
if HistoryMarkerKind.KEY in extra:
|
|
423
|
+
# Key presence, matching the grammar's boundary rule: a
|
|
424
|
+
# falsy kind is still chrome, never tool activity.
|
|
425
|
+
merged.append(_RawEntry(msg))
|
|
426
|
+
continue
|
|
427
|
+
|
|
428
|
+
tool_part = [
|
|
429
|
+
content
|
|
430
|
+
for content in contents
|
|
431
|
+
if (role == "assistant" and _is_tool_call(content)) or id(content) in standalone_hosted_results
|
|
432
|
+
]
|
|
433
|
+
|
|
434
|
+
if role == "tool" and not tool_part:
|
|
435
|
+
continue
|
|
436
|
+
|
|
437
|
+
has_text = _has_visible_text(contents)
|
|
438
|
+
embedded_itext = extra.get("_intermediate_text")
|
|
439
|
+
# _intermediate_text is a replay sidecar for intermediate text
|
|
440
|
+
# already shown before tool execution. Normally suppress it only
|
|
441
|
+
# when the same text is present in this message; the precomputed
|
|
442
|
+
# compatibility set also covers old cross-batch misattribution.
|
|
443
|
+
pending_embedded_itext = (
|
|
444
|
+
None
|
|
445
|
+
if message_index in legacy_duplicate_sidecars or _intermediate_text_is_visible(contents, embedded_itext)
|
|
446
|
+
else embedded_itext
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
if not tool_part:
|
|
450
|
+
if has_text:
|
|
451
|
+
merged.append(
|
|
452
|
+
_MergedAssistantEntry(
|
|
453
|
+
contents=list(contents),
|
|
454
|
+
additional_properties=extra,
|
|
455
|
+
)
|
|
456
|
+
)
|
|
457
|
+
else:
|
|
458
|
+
merged.append(_RawEntry(msg))
|
|
459
|
+
continue
|
|
460
|
+
|
|
461
|
+
call_replay_tools = iter(
|
|
462
|
+
self._build_replay_tool_calls(
|
|
463
|
+
[content for content in tool_part if id(content) not in standalone_hosted_results],
|
|
464
|
+
extra,
|
|
465
|
+
pairings,
|
|
466
|
+
annotations,
|
|
467
|
+
)
|
|
468
|
+
)
|
|
469
|
+
replay_tools = iter(
|
|
470
|
+
[
|
|
471
|
+
standalone_hosted_results[id(content)]
|
|
472
|
+
if id(content) in standalone_hosted_results
|
|
473
|
+
else next(call_replay_tools)
|
|
474
|
+
for content in tool_part
|
|
475
|
+
]
|
|
476
|
+
)
|
|
477
|
+
pending_text: list[Any] = []
|
|
478
|
+
pending_tools: list[ReplayToolCall] = []
|
|
479
|
+
text_before_next_tool = False
|
|
480
|
+
|
|
481
|
+
for content in contents:
|
|
482
|
+
if _is_tool_call(content) or id(content) in standalone_hosted_results:
|
|
483
|
+
if self._append_text_segment(merged, pending_text, extra, is_intermediate=True):
|
|
484
|
+
text_before_next_tool = True
|
|
485
|
+
pending_text.clear()
|
|
486
|
+
pending_tools.append(next(replay_tools))
|
|
487
|
+
else:
|
|
488
|
+
if pending_tools:
|
|
489
|
+
self._append_replay_tool_group(
|
|
490
|
+
merged,
|
|
491
|
+
list(pending_tools),
|
|
492
|
+
extra,
|
|
493
|
+
pending_embedded_itext,
|
|
494
|
+
has_preceding_text=text_before_next_tool,
|
|
495
|
+
)
|
|
496
|
+
if pending_embedded_itext:
|
|
497
|
+
pending_embedded_itext = None
|
|
498
|
+
pending_tools.clear()
|
|
499
|
+
text_before_next_tool = False
|
|
500
|
+
pending_text.append(content)
|
|
501
|
+
|
|
502
|
+
if pending_tools:
|
|
503
|
+
self._append_replay_tool_group(
|
|
504
|
+
merged,
|
|
505
|
+
list(pending_tools),
|
|
506
|
+
extra,
|
|
507
|
+
pending_embedded_itext,
|
|
508
|
+
has_preceding_text=text_before_next_tool,
|
|
509
|
+
)
|
|
510
|
+
trailing_text_is_final = bool(pending_text) and _hosted_trailing_text_is_final(contents)
|
|
511
|
+
self._append_text_segment(merged, pending_text, extra, is_intermediate=not trailing_text_is_final)
|
|
512
|
+
|
|
513
|
+
return self._coalesce_tool_groups(merged)
|
|
514
|
+
|
|
515
|
+
@staticmethod
|
|
516
|
+
def _standalone_hosted_replay_tool(
|
|
517
|
+
content: dict[str, Any],
|
|
518
|
+
*,
|
|
519
|
+
message_index: int,
|
|
520
|
+
content_index: int,
|
|
521
|
+
) -> ReplayToolCall:
|
|
522
|
+
"""Build one synthetic card from an unpaired canonical hosted result."""
|
|
523
|
+
result = _content_for_replay(content)
|
|
524
|
+
view = adapt_hosted_tool(None, result)
|
|
525
|
+
canonical_status = hosted_replay_status(view, has_result=True)
|
|
526
|
+
persisted_metadata = _replay_tool_metadata(content)
|
|
527
|
+
metadata = {**(persisted_metadata or {}), **view.metadata} or None
|
|
528
|
+
call_id = _call_id(content) or f"hosted-result:{message_index}:{content_index}"
|
|
529
|
+
return ReplayToolCall(
|
|
530
|
+
source=content,
|
|
531
|
+
name=view.tool_name or view.display_title or "hosted_tool",
|
|
532
|
+
call_id=call_id,
|
|
533
|
+
tool_kind=HOSTED_TOOL_DEFAULT_KIND_BY_FAMILY.get(view.family, ""),
|
|
534
|
+
arguments=view.arguments,
|
|
535
|
+
result=view.result_text or ("interrupted" if canonical_status == "interrupted" else ""),
|
|
536
|
+
metadata=metadata,
|
|
537
|
+
image_contents=[item.to_dict() for item in view.image_contents] or None,
|
|
538
|
+
provider_hosted=True,
|
|
539
|
+
hosted_family=view.family,
|
|
540
|
+
provider=view.provider,
|
|
541
|
+
provider_item_type=view.provider_item_type,
|
|
542
|
+
provider_status=canonical_status if canonical_status == "interrupted" else view.provider_status,
|
|
543
|
+
provider_call_id=view.provider_call_id,
|
|
544
|
+
canonical_status=canonical_status,
|
|
545
|
+
artifacts=_hosted_artifact_descriptors(view) or None,
|
|
546
|
+
duration_ms=_content_duration_ms(content),
|
|
547
|
+
started_at=_content_started_at(content),
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
@staticmethod
|
|
551
|
+
def _append_text_segment(
|
|
552
|
+
merged: list[MergedEntry],
|
|
553
|
+
contents: list[Any],
|
|
554
|
+
extra: dict[str, Any],
|
|
555
|
+
*,
|
|
556
|
+
is_intermediate: bool,
|
|
557
|
+
) -> bool:
|
|
558
|
+
if not _has_visible_text(contents):
|
|
559
|
+
return False
|
|
560
|
+
merged.append(
|
|
561
|
+
_MergedAssistantEntry(
|
|
562
|
+
contents=list(contents),
|
|
563
|
+
additional_properties=extra,
|
|
564
|
+
is_intermediate=is_intermediate,
|
|
565
|
+
)
|
|
566
|
+
)
|
|
567
|
+
return True
|
|
568
|
+
|
|
569
|
+
@staticmethod
|
|
570
|
+
def _append_replay_tool_group(
|
|
571
|
+
merged: list[MergedEntry],
|
|
572
|
+
replay_tools: list[ReplayToolCall],
|
|
573
|
+
extra: dict[str, Any],
|
|
574
|
+
embedded_itext: Any,
|
|
575
|
+
*,
|
|
576
|
+
has_preceding_text: bool,
|
|
577
|
+
) -> None:
|
|
578
|
+
batch_id = extra.get("_batch_id")
|
|
579
|
+
merge_target = None
|
|
580
|
+
if not has_preceding_text and batch_id is not None:
|
|
581
|
+
for merge_index in range(len(merged) - 1, -1, -1):
|
|
582
|
+
previous = merged[merge_index]
|
|
583
|
+
if (
|
|
584
|
+
isinstance(previous, _MergedAssistantEntry)
|
|
585
|
+
and previous.merged_role == "assistant_tools"
|
|
586
|
+
and previous.batch_id == batch_id
|
|
587
|
+
):
|
|
588
|
+
merge_target = merge_index
|
|
589
|
+
break
|
|
590
|
+
if not (isinstance(previous, _MergedAssistantEntry) and previous.merged_role == "assistant_tools"):
|
|
591
|
+
break
|
|
592
|
+
if merge_target is not None:
|
|
593
|
+
target = merged[merge_target]
|
|
594
|
+
if isinstance(target, _MergedAssistantEntry):
|
|
595
|
+
target.contents.extend(replay_tools)
|
|
596
|
+
if embedded_itext:
|
|
597
|
+
target.intermediate_texts.append(embedded_itext)
|
|
598
|
+
else:
|
|
599
|
+
intermediate_texts = [embedded_itext] if embedded_itext else []
|
|
600
|
+
merged.append(
|
|
601
|
+
_MergedAssistantEntry(
|
|
602
|
+
contents=list(replay_tools),
|
|
603
|
+
additional_properties=extra,
|
|
604
|
+
batch_id=batch_id,
|
|
605
|
+
intermediate_texts=intermediate_texts,
|
|
606
|
+
has_preceding_text=has_preceding_text,
|
|
607
|
+
merged_role="assistant_tools",
|
|
608
|
+
)
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
def _build_replay_tool_calls(
|
|
612
|
+
self,
|
|
613
|
+
tool_part: list[dict[str, Any]],
|
|
614
|
+
extra: dict[str, Any],
|
|
615
|
+
pairings: dict[int, _PairedToolData],
|
|
616
|
+
annotations: list[ReplayCompatibilityAnnotation],
|
|
617
|
+
) -> list[ReplayToolCall]:
|
|
618
|
+
approvals: dict[int, dict[str, Any]] = {}
|
|
619
|
+
for tool_call in tool_part:
|
|
620
|
+
content_approval = _approval_from_content(tool_call)
|
|
621
|
+
if content_approval is not None:
|
|
622
|
+
approvals[id(tool_call)] = content_approval
|
|
623
|
+
annotations.append(ReplayCompatibilityAnnotation(tool_call, {"_replay_approval": content_approval}))
|
|
624
|
+
|
|
625
|
+
approval = extra.get("_approval")
|
|
626
|
+
if isinstance(approval, dict):
|
|
627
|
+
tool_name = approval.get("tool_name", "")
|
|
628
|
+
for tool_call in tool_part:
|
|
629
|
+
if (
|
|
630
|
+
tool_call.get("_replay_approval") is not None
|
|
631
|
+
or id(tool_call) in approvals
|
|
632
|
+
or _approval_from_content(tool_call) is not None
|
|
633
|
+
):
|
|
634
|
+
continue
|
|
635
|
+
if not tool_name or tool_call.get("name", "") == tool_name:
|
|
636
|
+
approvals[id(tool_call)] = approval
|
|
637
|
+
annotations.append(ReplayCompatibilityAnnotation(tool_call, {"_replay_approval": approval}))
|
|
638
|
+
break
|
|
639
|
+
|
|
640
|
+
replay_tools: list[ReplayToolCall] = []
|
|
641
|
+
for tool_call in tool_part:
|
|
642
|
+
paired = pairings.get(id(tool_call), _PairedToolData())
|
|
643
|
+
hosted = paired.hosted_view
|
|
644
|
+
canonical_status = (
|
|
645
|
+
hosted_replay_status(hosted, has_result=paired.has_result)
|
|
646
|
+
if hosted is not None
|
|
647
|
+
else _local_replay_status(paired.metadata)
|
|
648
|
+
)
|
|
649
|
+
replay_result = paired.result or ("interrupted" if canonical_status == "interrupted" else "")
|
|
650
|
+
replay_tools.append(
|
|
651
|
+
ReplayToolCall(
|
|
652
|
+
source=tool_call,
|
|
653
|
+
name=hosted.tool_name if hosted is not None else str(tool_call.get("name", "tool")),
|
|
654
|
+
call_id=_call_id(tool_call),
|
|
655
|
+
tool_kind=(
|
|
656
|
+
HOSTED_TOOL_DEFAULT_KIND_BY_FAMILY.get(hosted.family, "")
|
|
657
|
+
if hosted is not None
|
|
658
|
+
else _persisted_tool_kind(tool_call)
|
|
659
|
+
),
|
|
660
|
+
arguments=hosted.arguments if hosted is not None else tool_call.get("arguments"),
|
|
661
|
+
result=replay_result,
|
|
662
|
+
metadata=paired.metadata,
|
|
663
|
+
image_contents=paired.image_contents,
|
|
664
|
+
file_snapshot=paired.file_snapshot,
|
|
665
|
+
approval=approvals.get(id(tool_call)) or tool_call.get("_replay_approval"),
|
|
666
|
+
provider_hosted=hosted is not None,
|
|
667
|
+
hosted_family=hosted.family if hosted is not None else "",
|
|
668
|
+
provider=hosted.provider if hosted is not None else "",
|
|
669
|
+
provider_item_type=hosted.provider_item_type if hosted is not None else "",
|
|
670
|
+
provider_status=(
|
|
671
|
+
canonical_status
|
|
672
|
+
if hosted is not None and canonical_status == "interrupted"
|
|
673
|
+
else hosted.provider_status
|
|
674
|
+
if hosted is not None
|
|
675
|
+
else ""
|
|
676
|
+
),
|
|
677
|
+
provider_call_id=hosted.provider_call_id if hosted is not None else "",
|
|
678
|
+
canonical_status=canonical_status,
|
|
679
|
+
artifacts=paired.artifacts,
|
|
680
|
+
duration_ms=paired.duration_ms,
|
|
681
|
+
started_at=paired.started_at,
|
|
682
|
+
)
|
|
683
|
+
)
|
|
684
|
+
return replay_tools
|
|
685
|
+
|
|
686
|
+
@staticmethod
|
|
687
|
+
def _coalesce_tool_groups(merged: list[MergedEntry]) -> list[MergedEntry]:
|
|
688
|
+
coalesced: list[MergedEntry] = []
|
|
689
|
+
for entry in merged:
|
|
690
|
+
can_merge = (
|
|
691
|
+
isinstance(entry, _MergedAssistantEntry)
|
|
692
|
+
and entry.merged_role == "assistant_tools"
|
|
693
|
+
and not entry.has_preceding_text
|
|
694
|
+
and not entry.intermediate_texts
|
|
695
|
+
and coalesced
|
|
696
|
+
and isinstance(coalesced[-1], _MergedAssistantEntry)
|
|
697
|
+
and coalesced[-1].merged_role == "assistant_tools"
|
|
698
|
+
)
|
|
699
|
+
if can_merge:
|
|
700
|
+
previous = coalesced[-1]
|
|
701
|
+
if isinstance(previous, _MergedAssistantEntry):
|
|
702
|
+
previous.contents.extend(entry.contents)
|
|
703
|
+
# The coalesced entry represents the complete sequence;
|
|
704
|
+
# message-level completion timing belongs to its latest
|
|
705
|
+
# assistant response, not the first tool cycle.
|
|
706
|
+
previous.additional_properties = entry.additional_properties
|
|
707
|
+
else:
|
|
708
|
+
coalesced.append(entry)
|
|
709
|
+
return coalesced
|
|
710
|
+
|
|
711
|
+
def _build_entries(
|
|
712
|
+
self,
|
|
713
|
+
merged: list[MergedEntry],
|
|
714
|
+
*,
|
|
715
|
+
suppress_marker_text: bool,
|
|
716
|
+
) -> list[ReplayEntry]:
|
|
717
|
+
entries: list[ReplayEntry] = []
|
|
718
|
+
seen_user_in_turn = False
|
|
719
|
+
retry_action_marker_index = _trailing_retry_marker_index(merged)
|
|
720
|
+
|
|
721
|
+
for msg_index, entry in enumerate(merged):
|
|
722
|
+
if isinstance(entry, _MergedAssistantEntry):
|
|
723
|
+
entries.append(
|
|
724
|
+
ReplayAgentMessage(
|
|
725
|
+
contents=list(entry.contents),
|
|
726
|
+
created_at=_message_created_at(entry.additional_properties),
|
|
727
|
+
is_intermediate=entry.is_intermediate,
|
|
728
|
+
intermediate_texts=list(entry.intermediate_texts),
|
|
729
|
+
duration_ms=_trajectory_duration_ms(entry.additional_properties),
|
|
730
|
+
)
|
|
731
|
+
)
|
|
732
|
+
continue
|
|
733
|
+
|
|
734
|
+
msg = entry.message
|
|
735
|
+
role = msg.get("role", "")
|
|
736
|
+
contents = _message_contents(msg)
|
|
737
|
+
extra = _message_extra(msg)
|
|
738
|
+
marker = extra.get(HistoryMarkerKind.KEY)
|
|
739
|
+
status_code = _recognized_status_code(extra)
|
|
740
|
+
|
|
741
|
+
if marker == HistoryMarkerKind.TURN:
|
|
742
|
+
entries.append(ReplayTurnMarker(turn_index_from_extra(extra)))
|
|
743
|
+
seen_user_in_turn = False
|
|
744
|
+
continue
|
|
745
|
+
|
|
746
|
+
if marker == HistoryMarkerKind.SUMMARY:
|
|
747
|
+
entries.append(
|
|
748
|
+
ReplayContextFold(
|
|
749
|
+
context_id=str(extra.get("_block_id", "")),
|
|
750
|
+
summary=_fold_summary(contents),
|
|
751
|
+
)
|
|
752
|
+
)
|
|
753
|
+
seen_user_in_turn = False
|
|
754
|
+
continue
|
|
755
|
+
|
|
756
|
+
if marker == HistoryMarkerKind.INTERRUPTED:
|
|
757
|
+
source = str(extra.get("_interrupted_by", "user"))
|
|
758
|
+
action_label = None
|
|
759
|
+
if msg_index == retry_action_marker_index:
|
|
760
|
+
action_label = (
|
|
761
|
+
INTERRUPTED_RETRY_ACTION_MESSAGE if source == "error" else INTERRUPTED_CONTINUE_ACTION_MESSAGE
|
|
762
|
+
)
|
|
763
|
+
entries.append(
|
|
764
|
+
ReplayInterrupted(
|
|
765
|
+
reason=_first_text(contents) or "Execution interrupted",
|
|
766
|
+
source=source,
|
|
767
|
+
action_label=action_label,
|
|
768
|
+
status_code=status_code,
|
|
769
|
+
status_count=_status_count(extra),
|
|
770
|
+
)
|
|
771
|
+
)
|
|
772
|
+
continue
|
|
773
|
+
|
|
774
|
+
if HistoryMarkerKind.KEY in extra:
|
|
775
|
+
# Status and unrecognized marker kinds (falsy included) fall
|
|
776
|
+
# through as plain text only — chrome payloads never surface
|
|
777
|
+
# as conversation tool activity.
|
|
778
|
+
if suppress_marker_text:
|
|
779
|
+
continue
|
|
780
|
+
text_contents = [
|
|
781
|
+
{**content, "text": sanitize_legacy_block(str(content.get("text", "")))}
|
|
782
|
+
for content in contents
|
|
783
|
+
if isinstance(content, dict) and content.get("type") == "text" and content.get("text")
|
|
784
|
+
]
|
|
785
|
+
if role == "assistant" and (text_contents or status_code is not None):
|
|
786
|
+
entries.append(
|
|
787
|
+
ReplayAgentMessage(
|
|
788
|
+
contents=text_contents,
|
|
789
|
+
created_at=_message_created_at(extra),
|
|
790
|
+
status_code=status_code,
|
|
791
|
+
status_count=_status_count(extra),
|
|
792
|
+
duration_ms=_trajectory_duration_ms(extra),
|
|
793
|
+
)
|
|
794
|
+
)
|
|
795
|
+
continue
|
|
796
|
+
|
|
797
|
+
if role == "user":
|
|
798
|
+
# A flagged synthetic ``continue`` nudge is a zero-content
|
|
799
|
+
# placeholder (the next resume deletes it) — don't render
|
|
800
|
+
# it, and don't flip ``seen_user_in_turn``. Legacy
|
|
801
|
+
# unflagged nudges are indistinguishable from a user-typed
|
|
802
|
+
# "continue" and keep rendering via the positional fallback.
|
|
803
|
+
if extra.get(HistoryMarkerKind.CONTINUATION_KEY):
|
|
804
|
+
continue
|
|
805
|
+
text = "\n".join(_text_parts(contents)).strip()
|
|
806
|
+
profile_switch_to = str(extra.get(HistoryMarkerKind.PROFILE_SWITCH_TO_KEY) or "")
|
|
807
|
+
if not text:
|
|
808
|
+
if profile_switch_to:
|
|
809
|
+
entries.append(ReplayProfileSwitch(profile_switch_to))
|
|
810
|
+
continue
|
|
811
|
+
is_injection = bool(extra.get(HistoryMarkerKind.INJECTED_KEY, False) or seen_user_in_turn)
|
|
812
|
+
entries.append(
|
|
813
|
+
ReplayUserMessage(
|
|
814
|
+
text=text,
|
|
815
|
+
contents=list(contents),
|
|
816
|
+
created_at=_message_created_at(extra),
|
|
817
|
+
is_injection=is_injection,
|
|
818
|
+
profile_switch_to=profile_switch_to,
|
|
819
|
+
duration_ms=_trajectory_duration_ms(extra),
|
|
820
|
+
)
|
|
821
|
+
)
|
|
822
|
+
if not is_injection:
|
|
823
|
+
seen_user_in_turn = True
|
|
824
|
+
continue
|
|
825
|
+
|
|
826
|
+
if role == "assistant":
|
|
827
|
+
entries.append(
|
|
828
|
+
ReplayAgentMessage(
|
|
829
|
+
contents=list(contents),
|
|
830
|
+
created_at=_message_created_at(extra),
|
|
831
|
+
duration_ms=_trajectory_duration_ms(extra),
|
|
832
|
+
)
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
return _mark_structured_output_completions(entries)
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def _mark_structured_output_completions(entries: list[ReplayEntry]) -> list[ReplayEntry]:
|
|
839
|
+
"""Mark a turn-ending successful hosted result that has no text response."""
|
|
840
|
+
candidate_index: int | None = None
|
|
841
|
+
|
|
842
|
+
def finalize_candidate() -> None:
|
|
843
|
+
nonlocal candidate_index
|
|
844
|
+
if candidate_index is None:
|
|
845
|
+
return
|
|
846
|
+
candidate = entries[candidate_index]
|
|
847
|
+
if isinstance(candidate, ReplayAgentMessage) and _has_successful_structured_tool(candidate.contents):
|
|
848
|
+
entries[candidate_index] = replace(candidate, structured_output_completed=True)
|
|
849
|
+
candidate_index = None
|
|
850
|
+
|
|
851
|
+
for index, entry in enumerate(entries):
|
|
852
|
+
if isinstance(entry, ReplayTurnMarker):
|
|
853
|
+
finalize_candidate()
|
|
854
|
+
continue
|
|
855
|
+
if isinstance(entry, ReplayUserMessage) and not entry.is_injection:
|
|
856
|
+
finalize_candidate()
|
|
857
|
+
continue
|
|
858
|
+
if isinstance(entry, ReplayInterrupted):
|
|
859
|
+
candidate_index = None
|
|
860
|
+
continue
|
|
861
|
+
if not isinstance(entry, ReplayAgentMessage) or entry.is_intermediate:
|
|
862
|
+
continue
|
|
863
|
+
if _has_visible_text(entry.contents) or any(
|
|
864
|
+
isinstance(content, ReplayToolCall) or (isinstance(content, dict) and _is_tool_call(content))
|
|
865
|
+
for content in entry.contents
|
|
866
|
+
):
|
|
867
|
+
candidate_index = index
|
|
868
|
+
|
|
869
|
+
finalize_candidate()
|
|
870
|
+
return entries
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def _has_successful_structured_tool(contents: list[Any]) -> bool:
|
|
874
|
+
"""Return whether replay contents carry a successful non-search hosted call."""
|
|
875
|
+
return any(
|
|
876
|
+
isinstance(content, ReplayToolCall)
|
|
877
|
+
and content.provider_hosted
|
|
878
|
+
and content.canonical_status == "completed"
|
|
879
|
+
and content.hosted_family
|
|
880
|
+
not in {HostedToolFamily.SEARCH, HostedToolFamily.FETCH, HostedToolFamily.TOOL_DISCOVERY}
|
|
881
|
+
for content in contents
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
class HistoryReplayRenderer:
|
|
886
|
+
"""Render planned persisted history into transcript widgets."""
|
|
887
|
+
|
|
888
|
+
def __init__(
|
|
889
|
+
self,
|
|
890
|
+
mount: ChatMountPort,
|
|
891
|
+
ui: TranscriptUiPort,
|
|
892
|
+
toc: TurnTocModel,
|
|
893
|
+
tool_kind_resolver: Callable[[str], str],
|
|
894
|
+
*,
|
|
895
|
+
default_profile: Callable[[], str],
|
|
896
|
+
localization: TranscriptLocalizationPort | None = None,
|
|
897
|
+
tool_widget_configurator: Callable[[Widget], None] | None = None,
|
|
898
|
+
) -> None:
|
|
899
|
+
self._mount = mount
|
|
900
|
+
self._ui = ui
|
|
901
|
+
self._toc = toc
|
|
902
|
+
self._tool_kind_resolver = tool_kind_resolver
|
|
903
|
+
self._default_profile = default_profile
|
|
904
|
+
self._tool_widget_configurator = tool_widget_configurator
|
|
905
|
+
self._resolve_message = localization.render if localization is not None else format_message
|
|
906
|
+
self._planner = HistoryReplayPlanner()
|
|
907
|
+
|
|
908
|
+
async def replay_history(
|
|
909
|
+
self,
|
|
910
|
+
messages: list[dict[str, Any]],
|
|
911
|
+
*,
|
|
912
|
+
initial_profile: str = "",
|
|
913
|
+
file_snapshots: dict[str, list[Any]] | None = None,
|
|
914
|
+
compressed_blocks: dict[str, CompressedBlock] | None = None,
|
|
915
|
+
) -> None:
|
|
916
|
+
"""Replay saved session messages into the chat transcript."""
|
|
917
|
+
await self._ui.dismiss_welcome_for_content()
|
|
918
|
+
self._ui.on_replay_started()
|
|
919
|
+
plan = self._planner.build_plan(messages, file_snapshots=file_snapshots)
|
|
920
|
+
plan.apply_compatibility_annotations()
|
|
921
|
+
replay_profile = initial_profile or self._default_profile()
|
|
922
|
+
seen_user_in_turn = False
|
|
923
|
+
widgets: list[Widget] = []
|
|
924
|
+
|
|
925
|
+
for entry in plan.entries:
|
|
926
|
+
if isinstance(entry, ReplayTurnMarker):
|
|
927
|
+
self._toc.apply_turn_marker(entry.turn_index)
|
|
928
|
+
seen_user_in_turn = False
|
|
929
|
+
continue
|
|
930
|
+
|
|
931
|
+
if isinstance(entry, ReplayContextFold):
|
|
932
|
+
block = compressed_blocks.get(entry.context_id) if compressed_blocks else None
|
|
933
|
+
if block:
|
|
934
|
+
replay_profile = await self._replay_compressed_block(block, replay_profile, widgets=widgets)
|
|
935
|
+
freed_messages = len(block.messages) if block else 0
|
|
936
|
+
turn_range = block.turn_range if block else (0, 0)
|
|
937
|
+
widgets.append(
|
|
938
|
+
ContextFoldWidget(
|
|
939
|
+
entry.context_id,
|
|
940
|
+
entry.summary,
|
|
941
|
+
freed_messages,
|
|
942
|
+
turn_range,
|
|
943
|
+
resolve_message=self._resolve_message,
|
|
944
|
+
)
|
|
945
|
+
)
|
|
946
|
+
seen_user_in_turn = False
|
|
947
|
+
continue
|
|
948
|
+
|
|
949
|
+
if isinstance(entry, ReplayInterrupted):
|
|
950
|
+
reason = self._resolve_status(entry.status_code, entry.status_count)
|
|
951
|
+
if reason is None:
|
|
952
|
+
reason = sanitize_legacy_block(entry.reason)
|
|
953
|
+
copy = resolve_interrupted_message_copy(reason, entry.source, self._resolve_message)
|
|
954
|
+
action_label = None
|
|
955
|
+
if entry.action_label is not None:
|
|
956
|
+
action_label = self._resolve_message(entry.action_label.bind())
|
|
957
|
+
widgets.append(
|
|
958
|
+
InterruptedMessage(copy.reason, entry.source, action_label=action_label, header=copy.header)
|
|
959
|
+
)
|
|
960
|
+
continue
|
|
961
|
+
|
|
962
|
+
if isinstance(entry, ReplayUserMessage):
|
|
963
|
+
if entry.profile_switch_to:
|
|
964
|
+
replay_profile = entry.profile_switch_to
|
|
965
|
+
is_injection = entry.is_injection or seen_user_in_turn
|
|
966
|
+
if is_injection and self._toc.has_items():
|
|
967
|
+
turn_id, _item = self._toc.add_injection(entry.text)
|
|
968
|
+
widget = UserMessage(
|
|
969
|
+
entry.text,
|
|
970
|
+
timestamp=format_message_created_at(entry.created_at),
|
|
971
|
+
is_injection=True,
|
|
972
|
+
contents=entry.contents,
|
|
973
|
+
)
|
|
974
|
+
else:
|
|
975
|
+
turn_id, _item = self._toc.begin_user_turn(entry.text)
|
|
976
|
+
widget = UserMessage(
|
|
977
|
+
entry.text,
|
|
978
|
+
timestamp=format_message_created_at(entry.created_at),
|
|
979
|
+
contents=entry.contents,
|
|
980
|
+
)
|
|
981
|
+
seen_user_in_turn = True
|
|
982
|
+
widget.id = turn_id
|
|
983
|
+
widgets.append(widget)
|
|
984
|
+
continue
|
|
985
|
+
|
|
986
|
+
if isinstance(entry, ReplayProfileSwitch):
|
|
987
|
+
replay_profile = entry.profile_name
|
|
988
|
+
continue
|
|
989
|
+
|
|
990
|
+
if isinstance(entry, ReplayAgentMessage):
|
|
991
|
+
widgets.extend(
|
|
992
|
+
AgentMessage(
|
|
993
|
+
itext,
|
|
994
|
+
is_final=True,
|
|
995
|
+
profile_name=replay_profile,
|
|
996
|
+
is_intermediate=True,
|
|
997
|
+
)
|
|
998
|
+
for itext in entry.intermediate_texts
|
|
999
|
+
if process_think_tags(itext, intermediate=True)
|
|
1000
|
+
)
|
|
1001
|
+
contents = entry.contents
|
|
1002
|
+
status_text = self._resolve_status(entry.status_code, entry.status_count)
|
|
1003
|
+
if status_text is not None:
|
|
1004
|
+
contents = [{"type": "text", "text": status_text}]
|
|
1005
|
+
await self._replay_assistant(
|
|
1006
|
+
contents,
|
|
1007
|
+
profile_name=replay_profile,
|
|
1008
|
+
is_intermediate=entry.is_intermediate,
|
|
1009
|
+
structured_output_completed=entry.structured_output_completed,
|
|
1010
|
+
created_at=entry.created_at,
|
|
1011
|
+
duration_ms=entry.duration_ms,
|
|
1012
|
+
widgets=widgets,
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
if widgets:
|
|
1016
|
+
await self._mount.mount_transcript_widgets(widgets)
|
|
1017
|
+
self._ui.after_replay_history_mounted()
|
|
1018
|
+
|
|
1019
|
+
def _resolve_status(self, status_code: str | None, count: int | None) -> str | None:
|
|
1020
|
+
if status_code is None:
|
|
1021
|
+
return None
|
|
1022
|
+
# A recognized code the display map does not cover, or a dynamic
|
|
1023
|
+
# semantic marker missing its structured fields, fails closed to the
|
|
1024
|
+
# persisted literal — same contract as an unrecognized code.
|
|
1025
|
+
definition = _STATUS_MESSAGE_DEFINITIONS.get(status_code)
|
|
1026
|
+
if definition is None:
|
|
1027
|
+
return None
|
|
1028
|
+
reference: MessageRef
|
|
1029
|
+
if status_code == HistoryMarkerKind.STATUS_AWAITING_SUB_AGENTS:
|
|
1030
|
+
if not count:
|
|
1031
|
+
return None
|
|
1032
|
+
reference = definition.bind(count=count)
|
|
1033
|
+
else:
|
|
1034
|
+
reference = definition.bind()
|
|
1035
|
+
return self._resolve_message(reference)
|
|
1036
|
+
|
|
1037
|
+
async def _replay_compressed_block(
|
|
1038
|
+
self,
|
|
1039
|
+
block: CompressedBlock,
|
|
1040
|
+
replay_profile: str,
|
|
1041
|
+
*,
|
|
1042
|
+
widgets: list[Widget] | None = None,
|
|
1043
|
+
) -> str:
|
|
1044
|
+
"""Render a compressed block through the canonical replay planner."""
|
|
1045
|
+
replay_widgets = widgets if widgets is not None else []
|
|
1046
|
+
serialized_messages = [message.to_dict() for message in block.messages]
|
|
1047
|
+
plan = self._planner.build_plan(serialized_messages, suppress_marker_text=True)
|
|
1048
|
+
plan.apply_compatibility_annotations()
|
|
1049
|
+
seen_user_in_turn = False
|
|
1050
|
+
for entry in plan.entries:
|
|
1051
|
+
if isinstance(entry, ReplayTurnMarker):
|
|
1052
|
+
self._toc.apply_turn_marker(entry.turn_index)
|
|
1053
|
+
seen_user_in_turn = False
|
|
1054
|
+
continue
|
|
1055
|
+
|
|
1056
|
+
if isinstance(entry, ReplayProfileSwitch):
|
|
1057
|
+
replay_profile = entry.profile_name
|
|
1058
|
+
continue
|
|
1059
|
+
|
|
1060
|
+
if isinstance(entry, ReplayUserMessage):
|
|
1061
|
+
if entry.profile_switch_to:
|
|
1062
|
+
replay_profile = entry.profile_switch_to
|
|
1063
|
+
is_injection = entry.is_injection or seen_user_in_turn
|
|
1064
|
+
if is_injection and self._toc.has_items():
|
|
1065
|
+
turn_id, _item = self._toc.add_injection(entry.text, compressed=True)
|
|
1066
|
+
widget = UserMessage(
|
|
1067
|
+
entry.text,
|
|
1068
|
+
timestamp=format_message_created_at(entry.created_at),
|
|
1069
|
+
compressed=True,
|
|
1070
|
+
is_injection=True,
|
|
1071
|
+
contents=entry.contents,
|
|
1072
|
+
)
|
|
1073
|
+
else:
|
|
1074
|
+
turn_id, _item = self._toc.begin_user_turn(entry.text, compressed=True)
|
|
1075
|
+
widget = UserMessage(
|
|
1076
|
+
entry.text,
|
|
1077
|
+
timestamp=format_message_created_at(entry.created_at),
|
|
1078
|
+
compressed=True,
|
|
1079
|
+
contents=entry.contents,
|
|
1080
|
+
)
|
|
1081
|
+
seen_user_in_turn = True
|
|
1082
|
+
widget.id = turn_id
|
|
1083
|
+
replay_widgets.append(widget)
|
|
1084
|
+
continue
|
|
1085
|
+
|
|
1086
|
+
if isinstance(entry, ReplayAgentMessage):
|
|
1087
|
+
replay_widgets.extend(
|
|
1088
|
+
AgentMessage(
|
|
1089
|
+
text,
|
|
1090
|
+
is_final=True,
|
|
1091
|
+
profile_name=replay_profile,
|
|
1092
|
+
is_intermediate=True,
|
|
1093
|
+
is_compressed=True,
|
|
1094
|
+
)
|
|
1095
|
+
for text in entry.intermediate_texts
|
|
1096
|
+
if process_think_tags(text, intermediate=True)
|
|
1097
|
+
)
|
|
1098
|
+
visible_contents = [
|
|
1099
|
+
content
|
|
1100
|
+
for content in entry.contents
|
|
1101
|
+
if not isinstance(content, ReplayToolCall)
|
|
1102
|
+
or content.provider_hosted
|
|
1103
|
+
or (
|
|
1104
|
+
content.source.get("type") == "function_call"
|
|
1105
|
+
and content.source.get("informational_only") is True
|
|
1106
|
+
)
|
|
1107
|
+
]
|
|
1108
|
+
await self._replay_assistant(
|
|
1109
|
+
visible_contents,
|
|
1110
|
+
profile_name=replay_profile,
|
|
1111
|
+
is_intermediate=entry.is_intermediate,
|
|
1112
|
+
structured_output_completed=entry.structured_output_completed,
|
|
1113
|
+
created_at=entry.created_at,
|
|
1114
|
+
duration_ms=entry.duration_ms,
|
|
1115
|
+
is_compressed=True,
|
|
1116
|
+
widgets=replay_widgets,
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
if widgets is None:
|
|
1120
|
+
await self._mount.mount_transcript_widgets(replay_widgets)
|
|
1121
|
+
return replay_profile
|
|
1122
|
+
|
|
1123
|
+
async def _replay_assistant(
|
|
1124
|
+
self,
|
|
1125
|
+
contents: list[Any],
|
|
1126
|
+
profile_name: str = "",
|
|
1127
|
+
is_intermediate: bool = False,
|
|
1128
|
+
structured_output_completed: bool = False,
|
|
1129
|
+
created_at: Any = None,
|
|
1130
|
+
duration_ms: int | None = None,
|
|
1131
|
+
is_compressed: bool = False,
|
|
1132
|
+
widgets: list[Widget] | None = None,
|
|
1133
|
+
) -> None:
|
|
1134
|
+
"""Replay a single assistant message, preserving text/tool-call order."""
|
|
1135
|
+
replay_widgets = widgets if widgets is not None else []
|
|
1136
|
+
pending_text: list[tuple[str, str | None]] = []
|
|
1137
|
+
pending_tools: list[ReplayToolCall | dict[str, Any]] = []
|
|
1138
|
+
timestamp = "" if is_intermediate else format_message_created_at(created_at)
|
|
1139
|
+
|
|
1140
|
+
async def flush_text() -> None:
|
|
1141
|
+
text = join_text_blocks(reconstruct_text_blocks(pending_text)).strip()
|
|
1142
|
+
pending_text.clear()
|
|
1143
|
+
if text and process_think_tags(text, intermediate=is_intermediate):
|
|
1144
|
+
replay_widgets.append(
|
|
1145
|
+
AgentMessage(
|
|
1146
|
+
text,
|
|
1147
|
+
is_final=True,
|
|
1148
|
+
profile_name=profile_name,
|
|
1149
|
+
is_intermediate=is_intermediate,
|
|
1150
|
+
is_compressed=is_compressed,
|
|
1151
|
+
timestamp=timestamp,
|
|
1152
|
+
duration_ms=None if is_intermediate else duration_ms,
|
|
1153
|
+
)
|
|
1154
|
+
)
|
|
1155
|
+
|
|
1156
|
+
async def flush_tools() -> None:
|
|
1157
|
+
if not pending_tools:
|
|
1158
|
+
return
|
|
1159
|
+
group = ToolGroup(live=False, tool_widget_configurator=self._tool_widget_configurator)
|
|
1160
|
+
for idx, tc in enumerate(pending_tools):
|
|
1161
|
+
if isinstance(tc, ReplayToolCall):
|
|
1162
|
+
name = tc.name
|
|
1163
|
+
call_id = tc.call_id
|
|
1164
|
+
args = tc.arguments
|
|
1165
|
+
result = tc.result or ""
|
|
1166
|
+
snapshot = tc.file_snapshot
|
|
1167
|
+
replay_approval = tc.approval
|
|
1168
|
+
replay_metadata = tc.metadata
|
|
1169
|
+
replay_image_contents = tc.image_contents
|
|
1170
|
+
replay_artifacts = tc.artifacts
|
|
1171
|
+
provider_hosted = tc.provider_hosted
|
|
1172
|
+
hosted_family = tc.hosted_family
|
|
1173
|
+
provider = tc.provider
|
|
1174
|
+
provider_item_type = tc.provider_item_type
|
|
1175
|
+
provider_status = tc.provider_status
|
|
1176
|
+
provider_call_id = tc.provider_call_id
|
|
1177
|
+
canonical_status = tc.canonical_status
|
|
1178
|
+
replay_duration_ms = tc.duration_ms
|
|
1179
|
+
replay_started_at = tc.started_at
|
|
1180
|
+
else:
|
|
1181
|
+
call_id = _call_id(tc)
|
|
1182
|
+
result = tc.get("_replay_result", "") or ""
|
|
1183
|
+
snapshot = tc.get("_file_snapshot")
|
|
1184
|
+
replay_approval = tc.get("_replay_approval")
|
|
1185
|
+
replay_metadata = tc.get("_replay_metadata")
|
|
1186
|
+
replay_image_contents = tc.get("_replay_image_contents")
|
|
1187
|
+
replay_artifacts = tc.get("_replay_artifacts")
|
|
1188
|
+
replay_duration_ms = _content_duration_ms(tc)
|
|
1189
|
+
replay_started_at = _content_started_at(tc)
|
|
1190
|
+
if _is_hosted_tool_call(tc):
|
|
1191
|
+
hosted = adapt_hosted_tool(_hosted_content_for_replay(tc))
|
|
1192
|
+
name = hosted.tool_name
|
|
1193
|
+
args = hosted.arguments
|
|
1194
|
+
provider_hosted = True
|
|
1195
|
+
hosted_family = hosted.family
|
|
1196
|
+
provider = hosted.provider
|
|
1197
|
+
provider_item_type = hosted.provider_item_type
|
|
1198
|
+
provider_status = hosted.provider_status
|
|
1199
|
+
provider_call_id = hosted.provider_call_id
|
|
1200
|
+
canonical_status = hosted_replay_status(hosted, has_result=False)
|
|
1201
|
+
else:
|
|
1202
|
+
name = tc.get("name", "tool")
|
|
1203
|
+
args = tc.get("arguments")
|
|
1204
|
+
provider_hosted = False
|
|
1205
|
+
hosted_family = ""
|
|
1206
|
+
provider = ""
|
|
1207
|
+
provider_item_type = ""
|
|
1208
|
+
provider_status = ""
|
|
1209
|
+
provider_call_id = ""
|
|
1210
|
+
canonical_status = _local_replay_status(replay_metadata)
|
|
1211
|
+
if isinstance(args, str):
|
|
1212
|
+
with suppress(json.JSONDecodeError, ValueError):
|
|
1213
|
+
args = json.loads(args)
|
|
1214
|
+
args_str = ""
|
|
1215
|
+
if isinstance(args, dict):
|
|
1216
|
+
args_str = json.dumps(args, ensure_ascii=False)
|
|
1217
|
+
elif args:
|
|
1218
|
+
args_str = str(args)
|
|
1219
|
+
tool_args = args if isinstance(args, dict) else None
|
|
1220
|
+
if isinstance(tc, ReplayToolCall):
|
|
1221
|
+
tool_kind = tc.tool_kind or self._tool_kind_resolver(name)
|
|
1222
|
+
elif provider_hosted:
|
|
1223
|
+
tool_kind = HOSTED_TOOL_DEFAULT_KIND_BY_FAMILY.get(hosted_family, "")
|
|
1224
|
+
else:
|
|
1225
|
+
tool_kind = _persisted_tool_kind(tc) or self._tool_kind_resolver(name)
|
|
1226
|
+
unique_key = f"{call_id or name}#{idx}"
|
|
1227
|
+
tool_approval = replay_approval.get("status") if isinstance(replay_approval, dict) else None
|
|
1228
|
+
if not isinstance(replay_metadata, dict):
|
|
1229
|
+
replay_metadata = None
|
|
1230
|
+
if not isinstance(replay_image_contents, list):
|
|
1231
|
+
replay_image_contents = None
|
|
1232
|
+
if not isinstance(replay_artifacts, list):
|
|
1233
|
+
replay_artifacts = None
|
|
1234
|
+
await group.add_collapsed_replay_tool(
|
|
1235
|
+
unique_key,
|
|
1236
|
+
name,
|
|
1237
|
+
tool_kind,
|
|
1238
|
+
args_str,
|
|
1239
|
+
args=tool_args,
|
|
1240
|
+
result=result,
|
|
1241
|
+
file_snapshot=snapshot,
|
|
1242
|
+
approval=tool_approval,
|
|
1243
|
+
metadata=replay_metadata,
|
|
1244
|
+
image_contents=replay_image_contents,
|
|
1245
|
+
artifacts=replay_artifacts,
|
|
1246
|
+
provider_hosted=provider_hosted,
|
|
1247
|
+
hosted_family=hosted_family,
|
|
1248
|
+
provider=provider,
|
|
1249
|
+
provider_item_type=provider_item_type,
|
|
1250
|
+
provider_status=provider_status,
|
|
1251
|
+
provider_call_id=provider_call_id,
|
|
1252
|
+
canonical_status=canonical_status,
|
|
1253
|
+
duration_ms=replay_duration_ms or 0,
|
|
1254
|
+
duration_known=replay_duration_ms is not None,
|
|
1255
|
+
timestamp=format_message_created_at(replay_started_at),
|
|
1256
|
+
lazy=True,
|
|
1257
|
+
)
|
|
1258
|
+
replay_widgets.append(group)
|
|
1259
|
+
pending_tools.clear()
|
|
1260
|
+
|
|
1261
|
+
for content in contents:
|
|
1262
|
+
is_tool = isinstance(content, ReplayToolCall) or _is_tool_call(content)
|
|
1263
|
+
if is_tool:
|
|
1264
|
+
if pending_text:
|
|
1265
|
+
await flush_text()
|
|
1266
|
+
pending_tools.append(content)
|
|
1267
|
+
else:
|
|
1268
|
+
if pending_tools:
|
|
1269
|
+
await flush_tools()
|
|
1270
|
+
if isinstance(content, dict) and content.get("type") == "text":
|
|
1271
|
+
text = content.get("text", "")
|
|
1272
|
+
block_id = text_block_id(content.get("additional_properties"))
|
|
1273
|
+
# Even an empty fragment can separate different item IDs.
|
|
1274
|
+
# Suppress empty blocks only after reconstruction.
|
|
1275
|
+
pending_text.append((text or "", block_id))
|
|
1276
|
+
elif isinstance(content, str):
|
|
1277
|
+
pending_text.append((content, None))
|
|
1278
|
+
|
|
1279
|
+
await flush_tools()
|
|
1280
|
+
await flush_text()
|
|
1281
|
+
if structured_output_completed and not is_intermediate:
|
|
1282
|
+
replay_widgets.append(
|
|
1283
|
+
AgentMessage(
|
|
1284
|
+
"✓",
|
|
1285
|
+
is_final=True,
|
|
1286
|
+
profile_name=profile_name,
|
|
1287
|
+
is_compressed=is_compressed,
|
|
1288
|
+
is_structured_completion=True,
|
|
1289
|
+
timestamp=timestamp,
|
|
1290
|
+
duration_ms=duration_ms,
|
|
1291
|
+
)
|
|
1292
|
+
)
|
|
1293
|
+
if widgets is None:
|
|
1294
|
+
await self._mount.mount_transcript_widgets(replay_widgets)
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
def _message_contents(message: dict[str, Any]) -> list[Any]:
|
|
1298
|
+
contents = message.get("contents", [])
|
|
1299
|
+
return contents if isinstance(contents, list) else []
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
def _message_extra(message: dict[str, Any]) -> dict[str, Any]:
|
|
1303
|
+
extra = message.get("additional_properties") or {}
|
|
1304
|
+
return extra if isinstance(extra, dict) else {}
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
def _trajectory_duration_ms(metadata: dict[str, Any]) -> int | None:
|
|
1308
|
+
timing = trajectory_timing_from_metadata(metadata)
|
|
1309
|
+
return timing["duration_ms"] if timing is not None else None
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
def _message_created_at(metadata: dict[str, Any]) -> Any:
|
|
1313
|
+
created_at = metadata.get(MESSAGE_CREATED_AT_KEY)
|
|
1314
|
+
if parse_created_at(created_at) is not None:
|
|
1315
|
+
return created_at
|
|
1316
|
+
timing = trajectory_timing_from_metadata(metadata)
|
|
1317
|
+
return timing["finished_at"] if timing is not None else None
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
def _content_timing(content: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
1321
|
+
if content is None:
|
|
1322
|
+
return None
|
|
1323
|
+
metadata = content.get("additional_properties")
|
|
1324
|
+
return trajectory_timing_from_metadata(metadata) if isinstance(metadata, dict) else None
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
def _content_duration_ms(content: dict[str, Any] | None) -> int | None:
|
|
1328
|
+
timing = _content_timing(content)
|
|
1329
|
+
return timing["duration_ms"] if timing is not None else None
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
def _content_started_at(content: dict[str, Any] | None) -> str | None:
|
|
1333
|
+
timing = _content_timing(content)
|
|
1334
|
+
return timing["started_at"] if timing is not None else None
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
def _recognized_status_code(extra: dict[str, Any]) -> str | None:
|
|
1338
|
+
status_code = extra.get(HistoryMarkerKind.STATUS_CODE_KEY)
|
|
1339
|
+
if isinstance(status_code, str) and status_code in HistoryMarkerKind.STATUS_CODES:
|
|
1340
|
+
return status_code
|
|
1341
|
+
return None
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def _status_count(extra: dict[str, Any]) -> int:
|
|
1345
|
+
invocation_ids = extra.get("_invocation_ids")
|
|
1346
|
+
return len(invocation_ids) if isinstance(invocation_ids, list) else 0
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
def _message_timestamp(extra: dict[str, Any] | None) -> str:
|
|
1350
|
+
if not extra:
|
|
1351
|
+
return ""
|
|
1352
|
+
return format_message_created_at(extra.get(MESSAGE_CREATED_AT_KEY))
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
def _is_tool_call(content: Any) -> bool:
|
|
1356
|
+
return isinstance(content, dict) and content.get("type") in (TOOL_CALL_CONTENT_TYPES | {LEGACY_CALL_CONTENT_TYPE})
|
|
1357
|
+
|
|
1358
|
+
|
|
1359
|
+
def _is_hosted_tool_call(content: dict[str, Any]) -> bool:
|
|
1360
|
+
return (
|
|
1361
|
+
bool(content.get("provider_hosted"))
|
|
1362
|
+
or content.get("type") in TOOL_CALL_CONTENT_TYPES - {"function_call"}
|
|
1363
|
+
or is_legacy_server_executed_informational_call(content)
|
|
1364
|
+
)
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
def _is_hosted_tool_result(content: dict[str, Any]) -> bool:
|
|
1368
|
+
"""Return whether persisted result content belongs to a hosted occurrence."""
|
|
1369
|
+
content_type = content.get("type")
|
|
1370
|
+
return content_type in TOOL_RESULT_CONTENT_TYPES - {"function_result"} or (
|
|
1371
|
+
content_type == "function_result" and bool(content.get("provider_hosted"))
|
|
1372
|
+
)
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
def _hosted_content_for_replay(content: dict[str, Any]) -> Content:
|
|
1376
|
+
"""Return a canonical in-memory hosted view without rewriting persisted data."""
|
|
1377
|
+
return normalize_hosted_replay_content(_content_for_replay(content))
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
def _content_for_replay(content: dict[str, Any]) -> Content:
|
|
1381
|
+
"""Decode persisted content without planner-only compatibility fields."""
|
|
1382
|
+
canonical = {key: value for key, value in content.items() if key not in _REPLAY_ONLY_CONTENT_KEYS}
|
|
1383
|
+
return Content.from_dict(canonical)
|
|
1384
|
+
|
|
1385
|
+
|
|
1386
|
+
def _local_replay_status(metadata: dict[str, Any] | None) -> str:
|
|
1387
|
+
"""Derive the non-hosted canonical state from persisted result metadata."""
|
|
1388
|
+
if metadata is not None and metadata.get(TOOL_INTERRUPTED_METADATA_KEY) is True:
|
|
1389
|
+
return "interrupted"
|
|
1390
|
+
return "completed"
|
|
1391
|
+
|
|
1392
|
+
|
|
1393
|
+
def _hosted_trailing_text_is_final(contents: list[Any]) -> bool:
|
|
1394
|
+
"""Mirror live planner semantics: text after terminal hosted work is final."""
|
|
1395
|
+
typed: list[Content] = []
|
|
1396
|
+
for content in contents:
|
|
1397
|
+
if not isinstance(content, dict):
|
|
1398
|
+
return False
|
|
1399
|
+
typed.append(_hosted_content_for_replay(content) if _is_tool_call(content) else _content_for_replay(content))
|
|
1400
|
+
if not any(_is_hosted_call(content) or _is_hosted_result(content) for content in typed):
|
|
1401
|
+
# Without hosted occurrences the planner reports any trailing text as final.
|
|
1402
|
+
return False
|
|
1403
|
+
return bool(ResponsePresentationPlan.from_messages([Message("assistant", typed)]).final_text)
|
|
1404
|
+
|
|
1405
|
+
|
|
1406
|
+
def _call_id(content: dict[str, Any]) -> str:
|
|
1407
|
+
if str(content.get("type", "")).startswith("image_generation_"):
|
|
1408
|
+
return str(content.get("image_id") or "")
|
|
1409
|
+
return str(content.get("call_id") or "")
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
def _hosted_artifact_descriptors(view: HostedToolView) -> list[dict[str, Any]]:
|
|
1413
|
+
descriptors: list[dict[str, Any]] = []
|
|
1414
|
+
for artifact in view.artifacts:
|
|
1415
|
+
descriptor: dict[str, Any] = {
|
|
1416
|
+
"id": artifact.file_id or artifact.vector_store_id or artifact.id or "",
|
|
1417
|
+
"path": artifact.uri or artifact.name or "",
|
|
1418
|
+
"mime": artifact.media_type or "",
|
|
1419
|
+
}
|
|
1420
|
+
size = artifact.additional_properties.get("size")
|
|
1421
|
+
if isinstance(size, int) and not isinstance(size, bool):
|
|
1422
|
+
descriptor["size"] = size
|
|
1423
|
+
descriptors.append({key: value for key, value in descriptor.items() if value != ""})
|
|
1424
|
+
return descriptors
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
def _approval_from_content(content: dict[str, Any]) -> dict[str, Any] | None:
|
|
1428
|
+
extra = content.get("additional_properties")
|
|
1429
|
+
if not isinstance(extra, dict):
|
|
1430
|
+
return None
|
|
1431
|
+
approval = extra.get("_approval")
|
|
1432
|
+
return approval if isinstance(approval, dict) else None
|
|
1433
|
+
|
|
1434
|
+
|
|
1435
|
+
def _replay_tool_metadata(content: dict[str, Any]) -> dict[str, Any] | None:
|
|
1436
|
+
extra = content.get("additional_properties")
|
|
1437
|
+
if not isinstance(extra, dict):
|
|
1438
|
+
return None
|
|
1439
|
+
metadata: dict[str, Any] = {}
|
|
1440
|
+
nested = extra.get(TOOL_RESULT_METADATA_KEY)
|
|
1441
|
+
if isinstance(nested, dict):
|
|
1442
|
+
metadata.update(nested)
|
|
1443
|
+
metadata.update({key: extra[key] for key in PERSISTED_RESULT_METADATA_KEYS if key in extra})
|
|
1444
|
+
return metadata or None
|
|
1445
|
+
|
|
1446
|
+
|
|
1447
|
+
def _persisted_tool_kind(content: dict[str, Any]) -> str:
|
|
1448
|
+
return persisted_tool_call_kind(content.get("additional_properties"))
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
def _replay_tool_images(content: dict[str, Any]) -> list[dict[str, Any]]:
|
|
1452
|
+
items = content.get("items")
|
|
1453
|
+
if not isinstance(items, list):
|
|
1454
|
+
return []
|
|
1455
|
+
|
|
1456
|
+
image_items: list[dict[str, Any]] = []
|
|
1457
|
+
for item in items:
|
|
1458
|
+
if not isinstance(item, dict):
|
|
1459
|
+
continue
|
|
1460
|
+
media_type = item.get("media_type")
|
|
1461
|
+
if media_type is None:
|
|
1462
|
+
extra = item.get("additional_properties")
|
|
1463
|
+
media_type = extra.get("media_type") if isinstance(extra, dict) else None
|
|
1464
|
+
if is_image_media_type(media_type):
|
|
1465
|
+
image_items.append(item)
|
|
1466
|
+
return image_items
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
def _fold_summary(contents: list[Any]) -> str:
|
|
1470
|
+
fallback = ""
|
|
1471
|
+
for content in contents:
|
|
1472
|
+
text = (
|
|
1473
|
+
content.get("text", "") if isinstance(content, dict) else str(content) if isinstance(content, str) else ""
|
|
1474
|
+
)
|
|
1475
|
+
if text and "Summary:" in text:
|
|
1476
|
+
return text.split("Summary:", 1)[1].strip()
|
|
1477
|
+
if text:
|
|
1478
|
+
fallback = text
|
|
1479
|
+
return fallback
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
def _first_text(contents: list[Any]) -> str:
|
|
1483
|
+
for content in contents:
|
|
1484
|
+
if isinstance(content, dict) and content.get("type") == "text":
|
|
1485
|
+
return str(content.get("text", ""))
|
|
1486
|
+
if isinstance(content, str):
|
|
1487
|
+
return content
|
|
1488
|
+
return ""
|
|
1489
|
+
|
|
1490
|
+
|
|
1491
|
+
def _has_visible_text(contents: list[Any]) -> bool:
|
|
1492
|
+
return any(
|
|
1493
|
+
(isinstance(content, dict) and content.get("type") == "text" and content.get("text"))
|
|
1494
|
+
or isinstance(content, str)
|
|
1495
|
+
for content in contents
|
|
1496
|
+
)
|
|
1497
|
+
|
|
1498
|
+
|
|
1499
|
+
def _intermediate_text_is_visible(contents: list[Any], intermediate_text: Any) -> bool:
|
|
1500
|
+
if not isinstance(intermediate_text, str) or not intermediate_text.strip():
|
|
1501
|
+
return False
|
|
1502
|
+
normalized = intermediate_text.strip()
|
|
1503
|
+
return any(text == normalized for text in _visible_text_variants(contents))
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
def _legacy_duplicate_intermediate_sidecars(messages: list[dict[str, Any]]) -> set[int]:
|
|
1507
|
+
"""Find sidecars misattributed to an adjacent persisted tool batch.
|
|
1508
|
+
|
|
1509
|
+
Older Chrys versions paired captured intermediate text to assistant
|
|
1510
|
+
messages positionally. Retry, resume, and mid-pass compaction could shift
|
|
1511
|
+
that pairing by one batch, leaving the real text in one assistant message
|
|
1512
|
+
and an identical ``_intermediate_text`` sidecar on the adjacent batch.
|
|
1513
|
+
|
|
1514
|
+
This compatibility logic should be deprecated once sessions written by
|
|
1515
|
+
those buggy versions no longer need replay support. Keep it deliberately
|
|
1516
|
+
narrow in the meantime: both messages must be batch-tagged tool calls and
|
|
1517
|
+
separated only by one or more tool-result messages.
|
|
1518
|
+
"""
|
|
1519
|
+
duplicates: set[int] = set()
|
|
1520
|
+
for index, message in enumerate(messages):
|
|
1521
|
+
if message.get("role") != "assistant":
|
|
1522
|
+
continue
|
|
1523
|
+
contents = _message_contents(message)
|
|
1524
|
+
if not any(_is_tool_call(content) for content in contents):
|
|
1525
|
+
continue
|
|
1526
|
+
extra = _message_extra(message)
|
|
1527
|
+
intermediate_text = extra.get("_intermediate_text")
|
|
1528
|
+
if "_batch_id" not in extra or _intermediate_text_is_visible(contents, intermediate_text):
|
|
1529
|
+
continue
|
|
1530
|
+
|
|
1531
|
+
for direction in (-1, 1):
|
|
1532
|
+
cursor = index + direction
|
|
1533
|
+
crossed_tool_result = False
|
|
1534
|
+
while 0 <= cursor < len(messages) and messages[cursor].get("role") == "tool":
|
|
1535
|
+
crossed_tool_result = True
|
|
1536
|
+
cursor += direction
|
|
1537
|
+
if not crossed_tool_result or not 0 <= cursor < len(messages):
|
|
1538
|
+
continue
|
|
1539
|
+
|
|
1540
|
+
adjacent = messages[cursor]
|
|
1541
|
+
if adjacent.get("role") != "assistant":
|
|
1542
|
+
continue
|
|
1543
|
+
adjacent_contents = _message_contents(adjacent)
|
|
1544
|
+
adjacent_extra = _message_extra(adjacent)
|
|
1545
|
+
if (
|
|
1546
|
+
"_batch_id" in adjacent_extra
|
|
1547
|
+
and any(_is_tool_call(content) for content in adjacent_contents)
|
|
1548
|
+
and _intermediate_text_is_visible(adjacent_contents, intermediate_text)
|
|
1549
|
+
):
|
|
1550
|
+
duplicates.add(index)
|
|
1551
|
+
break
|
|
1552
|
+
return duplicates
|
|
1553
|
+
|
|
1554
|
+
|
|
1555
|
+
def _visible_text_variants(contents: list[Any]) -> list[str]:
|
|
1556
|
+
variants: list[str] = []
|
|
1557
|
+
all_parts: list[str] = []
|
|
1558
|
+
segment_parts: list[str] = []
|
|
1559
|
+
# Legacy replay inserted newlines between every fragment, while live text
|
|
1560
|
+
# and session sidecar suppression concatenated without separators. Recognize
|
|
1561
|
+
# both historical forms for backward-compatible _intermediate_text matching.
|
|
1562
|
+
for content in contents:
|
|
1563
|
+
if isinstance(content, dict) and content.get("type") == "text":
|
|
1564
|
+
text = str(content.get("text", ""))
|
|
1565
|
+
elif isinstance(content, str):
|
|
1566
|
+
text = content
|
|
1567
|
+
else:
|
|
1568
|
+
if segment_parts:
|
|
1569
|
+
variants.append("\n".join(segment_parts).strip())
|
|
1570
|
+
variants.append("".join(segment_parts).strip())
|
|
1571
|
+
segment_parts.clear()
|
|
1572
|
+
continue
|
|
1573
|
+
if text:
|
|
1574
|
+
all_parts.append(text)
|
|
1575
|
+
segment_parts.append(text)
|
|
1576
|
+
if segment_parts:
|
|
1577
|
+
variants.append("\n".join(segment_parts).strip())
|
|
1578
|
+
variants.append("".join(segment_parts).strip())
|
|
1579
|
+
if all_parts:
|
|
1580
|
+
variants.append("\n".join(all_parts).strip())
|
|
1581
|
+
variants.append("".join(all_parts).strip())
|
|
1582
|
+
return [text for text in variants if text]
|
|
1583
|
+
|
|
1584
|
+
|
|
1585
|
+
def _text_parts(contents: list[Any]) -> list[str]:
|
|
1586
|
+
parts: list[str] = []
|
|
1587
|
+
for content in contents:
|
|
1588
|
+
if isinstance(content, dict) and content.get("type") == "text":
|
|
1589
|
+
parts.append(str(content.get("text", "")))
|
|
1590
|
+
elif isinstance(content, str):
|
|
1591
|
+
parts.append(content)
|
|
1592
|
+
return parts
|
|
1593
|
+
|
|
1594
|
+
|
|
1595
|
+
def _trailing_retry_marker_index(messages: list[MergedEntry]) -> int | None:
|
|
1596
|
+
for index in range(len(messages) - 1, -1, -1):
|
|
1597
|
+
entry = messages[index]
|
|
1598
|
+
if not isinstance(entry, _RawEntry):
|
|
1599
|
+
return None
|
|
1600
|
+
extra = _message_extra(entry.message)
|
|
1601
|
+
marker = extra.get(HistoryMarkerKind.KEY)
|
|
1602
|
+
if marker == HistoryMarkerKind.TURN:
|
|
1603
|
+
continue
|
|
1604
|
+
return index if marker == HistoryMarkerKind.INTERRUPTED else None
|
|
1605
|
+
return None
|