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,1315 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
3
|
+
# Contains code adapted from Microsoft Agent Framework (MIT License; see NOTICE).
|
|
4
|
+
|
|
5
|
+
"""Chrys-owned compaction primitives.
|
|
6
|
+
|
|
7
|
+
The group-annotation keys, grouping engine, incremental (re-)annotation,
|
|
8
|
+
token-count annotation, projection, and exclusion flag primitives live here.
|
|
9
|
+
``chrys.service.context.compaction``'s four-phase algorithm and the other consumers
|
|
10
|
+
(last-words, history restore, TUI rendering) read and write message metadata
|
|
11
|
+
exclusively through this module.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import base64
|
|
17
|
+
import binascii
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import math
|
|
21
|
+
import weakref
|
|
22
|
+
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
|
|
23
|
+
from contextlib import contextmanager
|
|
24
|
+
from contextvars import ContextVar
|
|
25
|
+
from copy import deepcopy
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import Any, Final, Literal, Protocol, cast, runtime_checkable
|
|
28
|
+
|
|
29
|
+
from chrys.foundation.errors import ProviderResponseError
|
|
30
|
+
from chrys.foundation.hosted_tools import HOSTED_WIRE_REPLAY_PROPERTY_KEYS
|
|
31
|
+
from chrys.foundation.models.history_markers import ANTHROPIC_THINKING_STRIPPED_KEY
|
|
32
|
+
from chrys.foundation.reasoning_origin import REASONING_ORIGIN_KEY
|
|
33
|
+
from chrys.foundation.text.images import inspect_image_dimensions
|
|
34
|
+
from chrys.foundation.tool_execution_stamp import EXECUTION_STAMP_KEY
|
|
35
|
+
|
|
36
|
+
from ._content import Content
|
|
37
|
+
from ._types import ChatResponse, Message
|
|
38
|
+
from .exchanges import (
|
|
39
|
+
TOOL_CALL_CONTENT_TYPES,
|
|
40
|
+
Exchange,
|
|
41
|
+
LiveAccessor,
|
|
42
|
+
is_result_only_message,
|
|
43
|
+
iter_exchanges,
|
|
44
|
+
)
|
|
45
|
+
from .exchanges import (
|
|
46
|
+
TOOL_RESULT_CONTENT_TYPES as TOOL_RESULT_CONTENT_TYPES, # re-exported; chrys.kernel imports it from here
|
|
47
|
+
)
|
|
48
|
+
from .images import is_image_content, is_image_data_uri, is_image_media_type
|
|
49
|
+
|
|
50
|
+
type GroupKind = Literal["system", "user", "assistant_text", "tool_call"]
|
|
51
|
+
|
|
52
|
+
_LIVE_ACCESSOR = LiveAccessor()
|
|
53
|
+
|
|
54
|
+
GROUP_ANNOTATION_KEY = "_group"
|
|
55
|
+
GROUP_ID_KEY = "id"
|
|
56
|
+
GROUP_KIND_KEY = "kind"
|
|
57
|
+
GROUP_INDEX_KEY = "index"
|
|
58
|
+
GROUP_HAS_REASONING_KEY = "has_reasoning"
|
|
59
|
+
GROUP_TOKEN_COUNT_KEY = "token_count"
|
|
60
|
+
GROUP_TOKEN_ESTIMATOR_VERSION_KEY = "token_estimator_v"
|
|
61
|
+
# Version 1 was implicit and serialized non-ASCII text as ``\\uXXXX`` escapes.
|
|
62
|
+
# Version 3 excluded provider-hosted wire replay mirrors. Version 4 additionally
|
|
63
|
+
# excludes opaque Responses reasoning state, which providers do not tokenize as
|
|
64
|
+
# ordinary prompt text.
|
|
65
|
+
TOKEN_ESTIMATOR_VERSION = 4
|
|
66
|
+
EXCLUDED_KEY = "_excluded"
|
|
67
|
+
EXCLUDE_REASON_KEY = "_exclude_reason"
|
|
68
|
+
SUMMARY_OF_MESSAGE_IDS_KEY = "_summary_of_message_ids"
|
|
69
|
+
SUMMARY_OF_GROUP_IDS_KEY = "_summary_of_group_ids"
|
|
70
|
+
SUMMARIZED_BY_SUMMARY_ID_KEY = "_summarized_by_summary_id"
|
|
71
|
+
|
|
72
|
+
_IMAGE_BASE_TOKENS = 85
|
|
73
|
+
_IMAGE_TILE_TOKENS = 170
|
|
74
|
+
_IMAGE_TILE_SIZE = 512
|
|
75
|
+
_IMAGE_FALLBACK_TOKENS = _IMAGE_BASE_TOKENS + (_IMAGE_TILE_TOKENS * 4)
|
|
76
|
+
_IMAGE_URI_TOKEN_PLACEHOLDER = "[image data omitted for token estimate]"
|
|
77
|
+
_TEXT_BUDGETED_ARGUMENT_CONTENT_TYPES = frozenset({"function_call", "search_tool_call", "mcp_server_tool_call"})
|
|
78
|
+
_PROJECTION_ATOMICITY_EXCLUDE_REASON = "partial tool-call group"
|
|
79
|
+
|
|
80
|
+
logger = logging.getLogger(__name__)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@runtime_checkable
|
|
84
|
+
class TokenizerProtocol(Protocol):
|
|
85
|
+
"""Protocol for token counters used by token-aware compaction strategies."""
|
|
86
|
+
|
|
87
|
+
def count_tokens(self, text: str) -> int:
|
|
88
|
+
"""Count tokens for a serialized message payload."""
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# The shared call/result content-type sets live in ``chrys.kernel.exchanges``
|
|
93
|
+
# (the exchange grammar needs them without importing this module); the import
|
|
94
|
+
# above re-exports them unchanged for every existing importer.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class LastWordsToolCallError(RuntimeError):
|
|
98
|
+
"""Raised when a last-words side call responds with tool-call content.
|
|
99
|
+
|
|
100
|
+
The side call's no-tools instruction should prevent this; when the model
|
|
101
|
+
ignores it, the requested tool calls must not be executed, so the side
|
|
102
|
+
call is treated as a failed attempt and retried by the caller.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# Set True in a response's ``additional_properties`` when the model filled its
|
|
107
|
+
# context window after it began answering: the response reads as cut off
|
|
108
|
+
# (``length``) and keeps what it wrote. A LAST_WORDS note fails instead
|
|
109
|
+
# (:func:`raise_if_context_window_filled`).
|
|
110
|
+
CONTEXT_WINDOW_FILLED_KEY: Final = "chrys_context_window_filled"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def raise_if_context_window_filled(response: ChatResponse[Any]) -> None:
|
|
114
|
+
"""Fail a LAST_WORDS note that filled the context window, as an overflow that is not retried.
|
|
115
|
+
|
|
116
|
+
The note was cut off and the same prompt fills the window again: the
|
|
117
|
+
overflow has the caller send a smaller one. Every LAST_WORDS side call
|
|
118
|
+
checks it, the completer's and the fallback's.
|
|
119
|
+
"""
|
|
120
|
+
if response.additional_properties.get(CONTEXT_WINDOW_FILLED_KEY) is True:
|
|
121
|
+
raise ProviderResponseError(
|
|
122
|
+
"model_context_window_exceeded", "The note filled the context window before it ended.", retryable=False
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class CompactionProjectionAtomicityError(RuntimeError):
|
|
127
|
+
"""Raised before strict compaction commits a partially projected tool-call group."""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# Marks the current task as being inside an internal side call (today: the
|
|
131
|
+
# Phase-4 last-words completer) issued below ``get_response``. Response
|
|
132
|
+
# instrumentation that publishes or persists per-response artifacts — e.g.
|
|
133
|
+
# the intermediate-text callbacks wired by the engine — must stay silent for
|
|
134
|
+
# these requests: the side-call response is consumed internally and never
|
|
135
|
+
# becomes part of the conversation. A ContextVar (not a client attribute)
|
|
136
|
+
# so concurrent live calls on the same client are unaffected.
|
|
137
|
+
_INTERNAL_SIDE_CALL: ContextVar[bool] = ContextVar("chrys_internal_side_call", default=False)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def in_internal_side_call() -> bool:
|
|
141
|
+
"""Whether the current task is inside an internal side call."""
|
|
142
|
+
return _INTERNAL_SIDE_CALL.get()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@contextmanager
|
|
146
|
+
def internal_side_call_scope() -> Iterator[None]:
|
|
147
|
+
"""Mark the enclosed request as an internal side call."""
|
|
148
|
+
token = _INTERNAL_SIDE_CALL.set(True)
|
|
149
|
+
try:
|
|
150
|
+
yield
|
|
151
|
+
finally:
|
|
152
|
+
_INTERNAL_SIDE_CALL.reset(token)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def messages_contain_tool_calls(messages: Sequence[Message]) -> bool:
|
|
156
|
+
"""Return whether any message carries tool-call content of any recognized type."""
|
|
157
|
+
return any(content.type in TOOL_CALL_CONTENT_TYPES for message in messages for content in message.contents)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@runtime_checkable
|
|
161
|
+
class LastWordsCompleter(Protocol):
|
|
162
|
+
"""Client-owned capability: scoped natural-completion side call.
|
|
163
|
+
|
|
164
|
+
Sends ``base_messages`` plus one appended user message containing
|
|
165
|
+
``instruction`` through the same client, route session, options, and
|
|
166
|
+
tool definitions as the live call — with ``tool_choice`` forced to
|
|
167
|
+
``"none"`` when tools are present — and returns only the assistant
|
|
168
|
+
text. Tool-call markup in plain text is additionally suppressed by the
|
|
169
|
+
instruction itself. Never persists anything into history and never
|
|
170
|
+
mutates the live call's options.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
async def complete_last_words(
|
|
174
|
+
self,
|
|
175
|
+
base_messages: Sequence[Message],
|
|
176
|
+
instruction: str,
|
|
177
|
+
*,
|
|
178
|
+
max_output_tokens: int,
|
|
179
|
+
on_usage: Callable[[Mapping[str, Any]], None] | None = None,
|
|
180
|
+
) -> str:
|
|
181
|
+
"""Run the side call over ``base_messages`` and return the note text.
|
|
182
|
+
|
|
183
|
+
``on_usage`` receives the raw provider-reported ``usage_details`` of
|
|
184
|
+
every side-call response as soon as it is observed — including
|
|
185
|
+
responses the completer subsequently rejects (a refused response
|
|
186
|
+
still spent real tokens). It must be cheap and non-raising.
|
|
187
|
+
"""
|
|
188
|
+
...
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass(frozen=True)
|
|
192
|
+
class CompactionCallContext:
|
|
193
|
+
"""Per-call capabilities a client passes into a compaction strategy.
|
|
194
|
+
|
|
195
|
+
Deliberately minimal: no client object, no options object by reference,
|
|
196
|
+
no route/session mutation surface. Strategies see exactly the
|
|
197
|
+
affordances listed here.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
last_words_completer: LastWordsCompleter | None = None
|
|
201
|
+
tool_definition_tokens: int = 0
|
|
202
|
+
"""Deprecated tools-only estimate retained for public API compatibility."""
|
|
203
|
+
request_overhead_tokens: int = 0
|
|
204
|
+
"""Estimate of all fixed request content outside the message list."""
|
|
205
|
+
next_exchange_operation_id: str | None = None
|
|
206
|
+
"""Exchange operation already reserved for the request being compacted."""
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@runtime_checkable
|
|
210
|
+
class CompactionAdmissionState(Protocol):
|
|
211
|
+
"""Strategy state required to admit an outbound model request safely."""
|
|
212
|
+
|
|
213
|
+
max_context_tokens: int
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def last_included_tokens(self) -> int: ...
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def system_overhead_tokens(self) -> int: ...
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def calibration_ratio(self) -> float: ...
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@runtime_checkable
|
|
226
|
+
class ContextOverflowSink(Protocol):
|
|
227
|
+
"""Strategy that takes a provider's verdict that the context window is full.
|
|
228
|
+
|
|
229
|
+
Separate from :class:`CompactionAdmissionState` on purpose: that
|
|
230
|
+
protocol's isinstance check gates the output-cap clamp, which a strategy
|
|
231
|
+
without this method must keep.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
def note_context_overflow(self, exc: BaseException | None = None) -> bool:
|
|
235
|
+
"""Make the next pass compact whatever the local estimate says.
|
|
236
|
+
|
|
237
|
+
*exc* is the provider's rejection, or ``None`` when no error carried
|
|
238
|
+
the verdict. Returns whether compacting and resending the same
|
|
239
|
+
request can help.
|
|
240
|
+
"""
|
|
241
|
+
...
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@runtime_checkable
|
|
245
|
+
class CompactionStrategy(Protocol):
|
|
246
|
+
"""Protocol for strategies run by ``BaseChatClient`` before model calls.
|
|
247
|
+
|
|
248
|
+
.. note:: **Breaking change (Phase-4 LAST_WORDS side call).** Strategies
|
|
249
|
+
are now invoked as ``strategy(messages, context)`` — two positional
|
|
250
|
+
arguments, where ``context`` is a :class:`CompactionCallContext` or
|
|
251
|
+
``None``. Strategies written against the earlier single-parameter
|
|
252
|
+
shape (``__call__(self, messages)``) fail loudly with ``TypeError``
|
|
253
|
+
and must add the ``context`` parameter (accepting and ignoring it is
|
|
254
|
+
valid: it only carries the optional cache-safe last-words completer).
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
async def __call__(self, messages: list[Message], context: CompactionCallContext | None = None) -> bool:
|
|
258
|
+
"""Mutate messages in place and return whether anything changed."""
|
|
259
|
+
...
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _has_content_type(message: Message, content_type: str) -> bool:
|
|
263
|
+
"""Return whether a message contains the requested content type."""
|
|
264
|
+
return any(content.type == content_type for content in message.contents)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _has_tool_call(message: Message) -> bool:
|
|
268
|
+
"""Return whether a message contains any recognized tool invocation."""
|
|
269
|
+
return any(content.type in TOOL_CALL_CONTENT_TYPES for content in message.contents)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _has_reasoning(message: Message) -> bool:
|
|
273
|
+
"""Return whether a message contains reasoning text."""
|
|
274
|
+
return _has_content_type(message, "text_reasoning")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _is_tool_call_assistant(message: Message) -> bool:
|
|
278
|
+
"""Return whether an assistant message contains a recognized tool invocation."""
|
|
279
|
+
return message.role == "assistant" and _has_tool_call(message)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _is_reasoning_only_assistant(message: Message) -> bool:
|
|
283
|
+
"""Return whether a non-empty assistant message contains only reasoning text."""
|
|
284
|
+
if message.role != "assistant" or not message.contents:
|
|
285
|
+
return False
|
|
286
|
+
return all(content.type == "text_reasoning" for content in message.contents)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _ensure_message_ids(
|
|
290
|
+
messages: list[Message], *, id_offset: int = 0, reserved_ids: Iterable[str] | None = None
|
|
291
|
+
) -> None:
|
|
292
|
+
"""Assign positional ids to messages that lack them, avoiding known collisions.
|
|
293
|
+
|
|
294
|
+
Ids are POSITIONAL (``msg_{index}``) and collision-checked only against
|
|
295
|
+
``messages`` plus ``reserved_ids``. A later pass can re-mint an id still
|
|
296
|
+
present in an archived ``compressed_msgs`` block, so ``message_id`` is not
|
|
297
|
+
a session-wide identity. For archived/live audit deduplication, follow the
|
|
298
|
+
contract in the module header of ``service/session/sub_agent_transcript.py``.
|
|
299
|
+
"""
|
|
300
|
+
existing_ids: set[str] = set(reserved_ids) if reserved_ids is not None else set()
|
|
301
|
+
existing_ids.update(message.message_id for message in messages if message.message_id)
|
|
302
|
+
for index, message in enumerate(messages):
|
|
303
|
+
if message.message_id:
|
|
304
|
+
continue
|
|
305
|
+
candidate = f"msg_{id_offset + index}"
|
|
306
|
+
if candidate in existing_ids:
|
|
307
|
+
counter = id_offset + len(messages)
|
|
308
|
+
candidate = f"msg_{counter}"
|
|
309
|
+
while candidate in existing_ids:
|
|
310
|
+
counter += 1
|
|
311
|
+
candidate = f"msg_{counter}"
|
|
312
|
+
message.message_id = candidate
|
|
313
|
+
existing_ids.add(candidate)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _group_id_for(message: Message, group_index: int) -> str:
|
|
317
|
+
"""Derive a group id from its message id, falling back to the group index."""
|
|
318
|
+
if message.message_id:
|
|
319
|
+
return f"group_{message.message_id}"
|
|
320
|
+
return f"group_index_{group_index}"
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def group_messages(
|
|
324
|
+
messages: list[Message], *, id_offset: int = 0, reserved_ids: Iterable[str] | None = None
|
|
325
|
+
) -> list[dict[str, Any]]:
|
|
326
|
+
"""Compute group spans and metadata for annotation.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
messages: The messages (or a slice of them) to group.
|
|
330
|
+
|
|
331
|
+
Keyword Args:
|
|
332
|
+
id_offset: Absolute starting index used when auto-assigning
|
|
333
|
+
``message_id`` values, so incremental annotation of a list slice
|
|
334
|
+
produces ids that stay unique across the full list.
|
|
335
|
+
reserved_ids: Message ids that already exist outside ``messages`` (for
|
|
336
|
+
example in a preserved prefix). Auto-assigned ids are guaranteed
|
|
337
|
+
not to collide with these, preventing duplicate ids across the
|
|
338
|
+
full list.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
Ordered list of lightweight span dicts with keys:
|
|
342
|
+
``group_id``, ``kind``, ``start_index``, ``end_index``,
|
|
343
|
+
``has_reasoning``.
|
|
344
|
+
|
|
345
|
+
Exchange boundaries — which output block answers which response run,
|
|
346
|
+
with history markers and non-assistant roles as hard boundaries — come
|
|
347
|
+
from the shared exchange grammar. Spans remain a secondary,
|
|
348
|
+
compaction-owned partition of each exchange (see
|
|
349
|
+
:func:`_exchange_spans`); messages outside every exchange group by
|
|
350
|
+
role as before.
|
|
351
|
+
"""
|
|
352
|
+
_ensure_message_ids(messages, id_offset=id_offset, reserved_ids=reserved_ids)
|
|
353
|
+
spans: list[dict[str, Any]] = []
|
|
354
|
+
|
|
355
|
+
def append_span(kind: GroupKind, start_index: int, end_index: int, *, has_reasoning: bool) -> None:
|
|
356
|
+
spans.append(
|
|
357
|
+
{
|
|
358
|
+
"group_id": _group_id_for(messages[start_index], len(spans)),
|
|
359
|
+
"kind": kind,
|
|
360
|
+
"start_index": start_index,
|
|
361
|
+
"end_index": end_index,
|
|
362
|
+
"has_reasoning": has_reasoning,
|
|
363
|
+
}
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
exchange_by_start = {
|
|
367
|
+
exchange.response_indices[0]: exchange for exchange in iter_exchanges(messages, _LIVE_ACCESSOR)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
i = 0
|
|
371
|
+
while i < len(messages):
|
|
372
|
+
exchange = exchange_by_start.get(i)
|
|
373
|
+
if exchange is not None:
|
|
374
|
+
for kind, start_index, end_index, has_reasoning in _exchange_spans(messages, exchange):
|
|
375
|
+
append_span(kind, start_index, end_index, has_reasoning=has_reasoning)
|
|
376
|
+
i = (exchange.output_indices or exchange.response_indices)[-1] + 1
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
current = messages[i]
|
|
380
|
+
if current.role == "system":
|
|
381
|
+
append_span("system", i, i, has_reasoning=_has_reasoning(current))
|
|
382
|
+
i += 1
|
|
383
|
+
continue
|
|
384
|
+
if current.role == "user":
|
|
385
|
+
append_span("user", i, i, has_reasoning=_has_reasoning(current))
|
|
386
|
+
i += 1
|
|
387
|
+
continue
|
|
388
|
+
if current.role == "tool":
|
|
389
|
+
# Orphan tool output answering no call run still groups as a run.
|
|
390
|
+
k = i + 1
|
|
391
|
+
while k < len(messages) and messages[k].role == "tool":
|
|
392
|
+
k += 1
|
|
393
|
+
append_span("tool_call", i, k - 1, has_reasoning=False)
|
|
394
|
+
i = k
|
|
395
|
+
continue
|
|
396
|
+
append_span("assistant_text", i, i, has_reasoning=_has_reasoning(current))
|
|
397
|
+
i += 1
|
|
398
|
+
|
|
399
|
+
return spans
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _exchange_spans(messages: list[Message], exchange: Exchange) -> list[tuple[GroupKind, int, int, bool]]:
|
|
403
|
+
"""Secondary partition of one exchange into span tuples.
|
|
404
|
+
|
|
405
|
+
A maximal run of call-carrying assistant siblings — reasoning-only
|
|
406
|
+
riders included, covering the OpenAI Responses shape where reasoning
|
|
407
|
+
and function_call contents are co-located as well as reasoning
|
|
408
|
+
prefixes in their own messages — forms one ``tool_call`` span. An
|
|
409
|
+
ordinary text-only sibling keeps its own span and splits call runs.
|
|
410
|
+
|
|
411
|
+
Once the exchange has results, calls and results must share ONE group:
|
|
412
|
+
compaction excludes group-wise, and a call grouped apart from its
|
|
413
|
+
result lets a partial exclusion send a result without its call (or a
|
|
414
|
+
call without its answered result) to the provider — a hard protocol
|
|
415
|
+
violation. So with an output block (tool-role or result-only assistant
|
|
416
|
+
messages, hosted MCP included), everything from the first call span
|
|
417
|
+
through the end of the output block fuses into one ``tool_call`` span,
|
|
418
|
+
absorbing interleaved non-call siblings; with only embedded results,
|
|
419
|
+
the call spans fuse through the last of them. Reasoning anywhere in
|
|
420
|
+
the fused range counts toward the group. Text before the first call
|
|
421
|
+
keeps its own span, and an exchange with no results at all keeps its
|
|
422
|
+
split spans.
|
|
423
|
+
"""
|
|
424
|
+
response_end = exchange.response_indices[-1]
|
|
425
|
+
segments: list[list[Any]] = []
|
|
426
|
+
|
|
427
|
+
i = exchange.response_indices[0]
|
|
428
|
+
while i <= response_end:
|
|
429
|
+
message = messages[i]
|
|
430
|
+
if _is_tool_call_assistant(message) or _is_reasoning_only_assistant(message):
|
|
431
|
+
run_start = i
|
|
432
|
+
saw_call = _is_tool_call_assistant(message)
|
|
433
|
+
i += 1
|
|
434
|
+
while i <= response_end and (
|
|
435
|
+
_is_tool_call_assistant(messages[i]) or _is_reasoning_only_assistant(messages[i])
|
|
436
|
+
):
|
|
437
|
+
saw_call = saw_call or _is_tool_call_assistant(messages[i])
|
|
438
|
+
i += 1
|
|
439
|
+
if saw_call:
|
|
440
|
+
has_reasoning = any(_has_reasoning(messages[k]) for k in range(run_start, i))
|
|
441
|
+
segments.append(["tool_call", run_start, i - 1, has_reasoning])
|
|
442
|
+
else:
|
|
443
|
+
# Reasoning-only siblings with no call to ride keep per-message spans.
|
|
444
|
+
segments.extend(["assistant_text", k, k, _has_reasoning(messages[k])] for k in range(run_start, i))
|
|
445
|
+
continue
|
|
446
|
+
segments.append(["assistant_text", i, i, _has_reasoning(message)])
|
|
447
|
+
i += 1
|
|
448
|
+
|
|
449
|
+
output = exchange.output_indices
|
|
450
|
+
first_call_pos = next((pos for pos, segment in enumerate(segments) if segment[0] == "tool_call"), None)
|
|
451
|
+
if first_call_pos is not None and (output or exchange.embedded_results):
|
|
452
|
+
fuse_end = output[-1] if output else max(segment[2] for segment in segments if segment[0] == "tool_call")
|
|
453
|
+
absorbed = [segment for segment in segments[first_call_pos:] if segment[1] <= fuse_end]
|
|
454
|
+
kept_tail = segments[first_call_pos + len(absorbed) :]
|
|
455
|
+
has_reasoning = any(segment[3] for segment in absorbed) or any(_has_reasoning(messages[k]) for k in output)
|
|
456
|
+
segments[first_call_pos:] = [
|
|
457
|
+
["tool_call", segments[first_call_pos][1], fuse_end, has_reasoning],
|
|
458
|
+
*kept_tail,
|
|
459
|
+
]
|
|
460
|
+
|
|
461
|
+
return [(kind, start_index, end_index, has_reasoning) for kind, start_index, end_index, has_reasoning in segments]
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _coerce_group_kind(value: object) -> GroupKind | None:
|
|
465
|
+
"""Coerce a raw annotation value into a known group kind.
|
|
466
|
+
|
|
467
|
+
The membership test intentionally preserves equality-scan semantics, so it
|
|
468
|
+
remains safe for unhashable values.
|
|
469
|
+
"""
|
|
470
|
+
if value in ("system", "user", "assistant_text", "tool_call"):
|
|
471
|
+
return cast("GroupKind", value)
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _read_group_annotation(message: Message) -> dict[str, Any] | None:
|
|
476
|
+
"""Read a validated group annotation.
|
|
477
|
+
|
|
478
|
+
Return ``None`` unless id, kind, index and has_reasoning are present and
|
|
479
|
+
well-typed, and any token count is an integer.
|
|
480
|
+
"""
|
|
481
|
+
raw_annotation = _read_group_annotation_raw(message)
|
|
482
|
+
if raw_annotation is None:
|
|
483
|
+
return None
|
|
484
|
+
|
|
485
|
+
group_id = raw_annotation.get(GROUP_ID_KEY)
|
|
486
|
+
group_kind = _coerce_group_kind(raw_annotation.get(GROUP_KIND_KEY))
|
|
487
|
+
group_index = raw_annotation.get(GROUP_INDEX_KEY)
|
|
488
|
+
has_reasoning = raw_annotation.get(GROUP_HAS_REASONING_KEY)
|
|
489
|
+
token_count = raw_annotation.get(GROUP_TOKEN_COUNT_KEY)
|
|
490
|
+
if token_count is not None and not isinstance(token_count, int):
|
|
491
|
+
return None
|
|
492
|
+
if (
|
|
493
|
+
not isinstance(group_id, str)
|
|
494
|
+
or group_kind is None
|
|
495
|
+
or not isinstance(group_index, int)
|
|
496
|
+
or not isinstance(has_reasoning, bool)
|
|
497
|
+
):
|
|
498
|
+
return None
|
|
499
|
+
|
|
500
|
+
return raw_annotation
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _read_group_annotation_raw(message: Message) -> dict[str, Any] | None:
|
|
504
|
+
"""Return the live annotation mapping for in-place mutation, or None when absent."""
|
|
505
|
+
annotation = message.additional_properties.get(GROUP_ANNOTATION_KEY)
|
|
506
|
+
if isinstance(annotation, Mapping):
|
|
507
|
+
return cast("dict[str, Any]", annotation)
|
|
508
|
+
return None
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _write_group_annotation(
|
|
512
|
+
message: Message,
|
|
513
|
+
*,
|
|
514
|
+
group_id: str,
|
|
515
|
+
kind: GroupKind,
|
|
516
|
+
index: int,
|
|
517
|
+
has_reasoning: bool,
|
|
518
|
+
) -> None:
|
|
519
|
+
"""Rewrite core group fields while preserving token counts and unknown fields.
|
|
520
|
+
|
|
521
|
+
This retains extensions such as ``SUMMARIZED_BY_SUMMARY_ID_KEY`` during reannotation.
|
|
522
|
+
"""
|
|
523
|
+
existing_raw_annotation = _read_group_annotation_raw(message)
|
|
524
|
+
unknown_fields: dict[str, Any] = {}
|
|
525
|
+
token_count: int | None = None
|
|
526
|
+
if existing_raw_annotation is not None:
|
|
527
|
+
raw_token_count = existing_raw_annotation.get(GROUP_TOKEN_COUNT_KEY)
|
|
528
|
+
if isinstance(raw_token_count, int) or raw_token_count is None:
|
|
529
|
+
token_count = raw_token_count
|
|
530
|
+
unknown_fields = {
|
|
531
|
+
key: value
|
|
532
|
+
for key, value in existing_raw_annotation.items()
|
|
533
|
+
if key
|
|
534
|
+
not in {
|
|
535
|
+
GROUP_ID_KEY,
|
|
536
|
+
GROUP_KIND_KEY,
|
|
537
|
+
GROUP_INDEX_KEY,
|
|
538
|
+
GROUP_HAS_REASONING_KEY,
|
|
539
|
+
GROUP_TOKEN_COUNT_KEY,
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
annotation = {
|
|
544
|
+
GROUP_ID_KEY: group_id,
|
|
545
|
+
GROUP_KIND_KEY: kind,
|
|
546
|
+
GROUP_INDEX_KEY: index,
|
|
547
|
+
GROUP_HAS_REASONING_KEY: has_reasoning,
|
|
548
|
+
GROUP_TOKEN_COUNT_KEY: token_count,
|
|
549
|
+
}
|
|
550
|
+
annotation.update(unknown_fields)
|
|
551
|
+
message.additional_properties[GROUP_ANNOTATION_KEY] = annotation
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _group_id(message: Message) -> str | None:
|
|
555
|
+
"""Read the group id from a valid annotation, or None when unavailable."""
|
|
556
|
+
annotation = _read_group_annotation(message)
|
|
557
|
+
if annotation is None:
|
|
558
|
+
return None
|
|
559
|
+
group_id = annotation.get(GROUP_ID_KEY)
|
|
560
|
+
return group_id if isinstance(group_id, str) else None
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _group_kind(message: Message) -> GroupKind | None:
|
|
564
|
+
"""Read the group kind from a valid annotation, or None when unavailable."""
|
|
565
|
+
annotation = _read_group_annotation(message)
|
|
566
|
+
if annotation is None:
|
|
567
|
+
return None
|
|
568
|
+
return _coerce_group_kind(annotation.get(GROUP_KIND_KEY))
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _group_index(message: Message) -> int | None:
|
|
572
|
+
"""Read the group index from a valid annotation, or None when unavailable."""
|
|
573
|
+
annotation = _read_group_annotation(message)
|
|
574
|
+
if annotation is None:
|
|
575
|
+
return None
|
|
576
|
+
group_index = annotation.get(GROUP_INDEX_KEY)
|
|
577
|
+
return group_index if isinstance(group_index, int) else None
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _token_count(message: Message) -> int | None:
|
|
581
|
+
"""Read a token count only when its annotation uses the current estimator version."""
|
|
582
|
+
annotation = _read_group_annotation(message)
|
|
583
|
+
if annotation is None:
|
|
584
|
+
return None
|
|
585
|
+
estimator_version = annotation.get(GROUP_TOKEN_ESTIMATOR_VERSION_KEY)
|
|
586
|
+
if (
|
|
587
|
+
not isinstance(estimator_version, int)
|
|
588
|
+
or isinstance(estimator_version, bool)
|
|
589
|
+
or estimator_version != TOKEN_ESTIMATOR_VERSION
|
|
590
|
+
):
|
|
591
|
+
return None
|
|
592
|
+
token_count = annotation.get(GROUP_TOKEN_COUNT_KEY)
|
|
593
|
+
return token_count if isinstance(token_count, int) else None
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _write_token_count(message: Message, token_count: int) -> None:
|
|
597
|
+
"""Store a token count and estimator version in the existing annotation.
|
|
598
|
+
|
|
599
|
+
Silently do nothing when no annotation mapping exists.
|
|
600
|
+
"""
|
|
601
|
+
annotation = _read_group_annotation_raw(message)
|
|
602
|
+
if annotation is None:
|
|
603
|
+
return
|
|
604
|
+
annotation[GROUP_TOKEN_COUNT_KEY] = token_count
|
|
605
|
+
annotation[GROUP_TOKEN_ESTIMATOR_VERSION_KEY] = TOKEN_ESTIMATOR_VERSION
|
|
606
|
+
message.additional_properties[GROUP_ANNOTATION_KEY] = annotation
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _ordered_group_ids_from_annotations(messages: Sequence[Message]) -> list[str]:
|
|
610
|
+
"""Collect distinct annotated group ids in first-occurrence order."""
|
|
611
|
+
ordered_group_ids: list[str] = []
|
|
612
|
+
seen: set[str] = set()
|
|
613
|
+
for message in messages:
|
|
614
|
+
group_id = _group_id(message)
|
|
615
|
+
if group_id is not None and group_id not in seen:
|
|
616
|
+
seen.add(group_id)
|
|
617
|
+
ordered_group_ids.append(group_id)
|
|
618
|
+
return ordered_group_ids
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _first_untokenized_index(messages: Sequence[Message]) -> int | None:
|
|
622
|
+
"""Find the first message without a current token estimate, or None when all have one."""
|
|
623
|
+
for index, message in enumerate(messages):
|
|
624
|
+
if _token_count(message) is None:
|
|
625
|
+
return index
|
|
626
|
+
return None
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _first_annotation_gaps(
|
|
630
|
+
messages: Sequence[Message],
|
|
631
|
+
*,
|
|
632
|
+
include_tokens: bool,
|
|
633
|
+
) -> tuple[int | None, int | None]:
|
|
634
|
+
"""Scan until the first missing group annotation or requested token count."""
|
|
635
|
+
first_unannotated: int | None = None
|
|
636
|
+
first_untokenized: int | None = None
|
|
637
|
+
for index, message in enumerate(messages):
|
|
638
|
+
missing_group_annotation = first_unannotated is None and _group_id(message) is None
|
|
639
|
+
missing_token_annotation = include_tokens and first_untokenized is None and _token_count(message) is None
|
|
640
|
+
|
|
641
|
+
if missing_group_annotation:
|
|
642
|
+
first_unannotated = index
|
|
643
|
+
if missing_token_annotation:
|
|
644
|
+
first_untokenized = index
|
|
645
|
+
|
|
646
|
+
if missing_group_annotation or missing_token_annotation:
|
|
647
|
+
break
|
|
648
|
+
return first_unannotated, first_untokenized
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _reannotation_start(messages: Sequence[Message], index: int) -> int:
|
|
652
|
+
"""Back the start index up to a position where regrouping is exchange-safe.
|
|
653
|
+
|
|
654
|
+
First to the beginning of the group the preceding message belongs to,
|
|
655
|
+
then — while the rewound position can still sit inside an exchange
|
|
656
|
+
begun earlier — further back along the exchange's backward shape:
|
|
657
|
+
output-block members (tool-role or result-only assistant) may follow
|
|
658
|
+
output or response-run members, response-run members only other
|
|
659
|
+
response-run members. Call/result fusion spans whole exchanges, so a
|
|
660
|
+
rewind stopping inside one regroups the suffix without its earlier
|
|
661
|
+
call spans — a result landing after a previously annotated
|
|
662
|
+
``[call, text]`` prefix would freeze the split grouping and leave the
|
|
663
|
+
call excludable apart from its result. A response-run member preceded
|
|
664
|
+
by an output-block member marks a COMPLETED earlier exchange, and
|
|
665
|
+
user/system roles and history markers are boundaries no exchange
|
|
666
|
+
crosses; stopping there keeps re-annotation per-exchange — walking
|
|
667
|
+
past finished exchanges would rewrite their annotations on every pass
|
|
668
|
+
and turn a long tool loop's incremental annotation quadratic.
|
|
669
|
+
"""
|
|
670
|
+
if index <= 0:
|
|
671
|
+
return 0
|
|
672
|
+
previous_index = index - 1
|
|
673
|
+
previous_group_id = _group_id(messages[previous_index])
|
|
674
|
+
previous_group_index = _group_index(messages[previous_index])
|
|
675
|
+
if previous_group_id is not None:
|
|
676
|
+
# Key on the group OCCURRENCE (id AND index): group ids are minted
|
|
677
|
+
# from the start message's id, so preset message_id collisions can
|
|
678
|
+
# give adjacent groups the same id — an id-only walk would cross
|
|
679
|
+
# them before the exchange-shape boundary check below runs.
|
|
680
|
+
while previous_index > 0:
|
|
681
|
+
if (
|
|
682
|
+
_group_id(messages[previous_index - 1]) != previous_group_id
|
|
683
|
+
or _group_index(messages[previous_index - 1]) != previous_group_index
|
|
684
|
+
):
|
|
685
|
+
break
|
|
686
|
+
previous_index -= 1
|
|
687
|
+
while previous_index > 0:
|
|
688
|
+
current = messages[previous_index]
|
|
689
|
+
preceding = messages[previous_index - 1]
|
|
690
|
+
if _is_output_block_member(current):
|
|
691
|
+
if not (_is_output_block_member(preceding) or _is_response_run_member(preceding)):
|
|
692
|
+
break
|
|
693
|
+
elif _is_response_run_member(current):
|
|
694
|
+
if not _is_response_run_member(preceding):
|
|
695
|
+
break
|
|
696
|
+
else:
|
|
697
|
+
break
|
|
698
|
+
previous_index -= 1
|
|
699
|
+
return previous_index
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _is_output_block_member(message: Message) -> bool:
|
|
703
|
+
"""Markerless tool-role or result-only assistant message — the shapes an
|
|
704
|
+
exchange's output block is made of."""
|
|
705
|
+
if _LIVE_ACCESSOR.has_marker(message):
|
|
706
|
+
return False
|
|
707
|
+
return message.role == "tool" or is_result_only_message(message, _LIVE_ACCESSOR)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def _is_response_run_member(message: Message) -> bool:
|
|
711
|
+
"""Markerless assistant message that is not result-only — the shapes a
|
|
712
|
+
response run is made of."""
|
|
713
|
+
return (
|
|
714
|
+
message.role == "assistant"
|
|
715
|
+
and not _LIVE_ACCESSOR.has_marker(message)
|
|
716
|
+
and not is_result_only_message(message, _LIVE_ACCESSOR)
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def annotate_message_groups(
|
|
721
|
+
messages: list[Message],
|
|
722
|
+
*,
|
|
723
|
+
from_index: int | None = None,
|
|
724
|
+
force_reannotate: bool = False,
|
|
725
|
+
tokenizer: TokenizerProtocol | None = None,
|
|
726
|
+
) -> list[str]:
|
|
727
|
+
"""Annotate message groups while reusing existing annotations when possible.
|
|
728
|
+
|
|
729
|
+
By default, the function re-annotates only the suffix that contains new
|
|
730
|
+
messages and keeps previously annotated prefixes untouched. When a
|
|
731
|
+
``tokenizer`` is provided, token-count annotations are also populated
|
|
732
|
+
incrementally.
|
|
733
|
+
"""
|
|
734
|
+
if not messages:
|
|
735
|
+
return []
|
|
736
|
+
|
|
737
|
+
if force_reannotate:
|
|
738
|
+
start_index = 0
|
|
739
|
+
elif from_index is not None:
|
|
740
|
+
start_index = max(0, min(from_index, len(messages) - 1))
|
|
741
|
+
else:
|
|
742
|
+
first_unannotated_index, first_untokenized_index = _first_annotation_gaps(
|
|
743
|
+
messages,
|
|
744
|
+
include_tokens=tokenizer is not None,
|
|
745
|
+
)
|
|
746
|
+
candidate_starts = [index for index in (first_unannotated_index, first_untokenized_index) if index is not None]
|
|
747
|
+
if not candidate_starts:
|
|
748
|
+
return _ordered_group_ids_from_annotations(messages)
|
|
749
|
+
start_index = min(candidate_starts)
|
|
750
|
+
|
|
751
|
+
start_index = _reannotation_start(messages, start_index)
|
|
752
|
+
|
|
753
|
+
# Continue group indices from the preserved prefix when only re-annotating a suffix.
|
|
754
|
+
group_index_offset = 0
|
|
755
|
+
if start_index > 0:
|
|
756
|
+
previous_group_index = _group_index(messages[start_index - 1])
|
|
757
|
+
if previous_group_index is not None:
|
|
758
|
+
group_index_offset = previous_group_index + 1
|
|
759
|
+
|
|
760
|
+
reserved_ids = {message.message_id for message in messages[:start_index] if message.message_id}
|
|
761
|
+
spans = group_messages(messages[start_index:], id_offset=start_index, reserved_ids=reserved_ids)
|
|
762
|
+
for span_index, span in enumerate(spans):
|
|
763
|
+
group_id = str(span["group_id"])
|
|
764
|
+
kind = _coerce_group_kind(span["kind"])
|
|
765
|
+
if kind is None:
|
|
766
|
+
raise ValueError(f"Unexpected group kind in span: {span['kind']}")
|
|
767
|
+
local_start_index = int(span["start_index"])
|
|
768
|
+
local_end_index = int(span["end_index"])
|
|
769
|
+
has_reasoning = bool(span["has_reasoning"])
|
|
770
|
+
for idx in range(start_index + local_start_index, start_index + local_end_index + 1):
|
|
771
|
+
message = messages[idx]
|
|
772
|
+
_write_group_annotation(
|
|
773
|
+
message,
|
|
774
|
+
group_id=group_id,
|
|
775
|
+
kind=kind,
|
|
776
|
+
index=group_index_offset + span_index,
|
|
777
|
+
has_reasoning=has_reasoning,
|
|
778
|
+
)
|
|
779
|
+
message.additional_properties.setdefault(EXCLUDED_KEY, False)
|
|
780
|
+
if tokenizer is not None and _token_count(message) is None:
|
|
781
|
+
_write_token_count(message, _message_token_estimate(message, tokenizer))
|
|
782
|
+
return _ordered_group_ids_from_annotations(messages)
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def _serialize_content(content: Content) -> dict[str, Any]:
|
|
786
|
+
"""Serialize content for token estimation, excluding duplicate and opaque replay data."""
|
|
787
|
+
payload = content.to_dict(exclude_none=True)
|
|
788
|
+
payload.pop("raw_representation", None)
|
|
789
|
+
additional_properties = payload.get("additional_properties")
|
|
790
|
+
if isinstance(additional_properties, dict):
|
|
791
|
+
additional_properties.pop(EXECUTION_STAMP_KEY, None)
|
|
792
|
+
additional_properties.pop(REASONING_ORIGIN_KEY, None)
|
|
793
|
+
additional_properties.pop(ANTHROPIC_THINKING_STRIPPED_KEY, None)
|
|
794
|
+
for key in HOSTED_WIRE_REPLAY_PROPERTY_KEYS:
|
|
795
|
+
additional_properties.pop(key, None)
|
|
796
|
+
# ``items`` mirrors ``result`` for function_result content; exclude it
|
|
797
|
+
# to avoid double-counting tokens during estimation.
|
|
798
|
+
payload.pop("items", None)
|
|
799
|
+
# Responses reasoning items carry server-encrypted replay state alongside
|
|
800
|
+
# an item id. Although this blob is sent back to the provider, it is opaque
|
|
801
|
+
# state rather than prompt text; tokenizing its base64 representation can
|
|
802
|
+
# overestimate the context by thousands of tokens. Id-less protected data
|
|
803
|
+
# remains included because chat-completions dialects use it for replayable
|
|
804
|
+
# reasoning_content/reasoning_details payloads.
|
|
805
|
+
if content.type == "text_reasoning" and content.id is not None:
|
|
806
|
+
payload.pop("protected_data", None)
|
|
807
|
+
return _redact_serialized_image_uris(payload)
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def _redact_serialized_image_uris(value: Any) -> Any:
|
|
811
|
+
if isinstance(value, dict):
|
|
812
|
+
redacted = {
|
|
813
|
+
key: nested
|
|
814
|
+
if key == "arguments" and _serialized_arguments_are_text_budgeted(value)
|
|
815
|
+
else _redact_serialized_image_uris(nested)
|
|
816
|
+
for key, nested in value.items()
|
|
817
|
+
}
|
|
818
|
+
if _serialized_dict_is_image_content(redacted) and "uri" in redacted:
|
|
819
|
+
redacted["uri"] = _IMAGE_URI_TOKEN_PLACEHOLDER
|
|
820
|
+
return redacted
|
|
821
|
+
if isinstance(value, list):
|
|
822
|
+
return [_redact_serialized_image_uris(item) for item in value]
|
|
823
|
+
return value
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def _serialized_dict_is_image_content(value: Mapping[Any, Any]) -> bool:
|
|
827
|
+
content_type = value.get("type")
|
|
828
|
+
if content_type not in ("data", "uri"):
|
|
829
|
+
return False
|
|
830
|
+
return (
|
|
831
|
+
is_image_media_type(value.get("media_type"))
|
|
832
|
+
or is_image_media_type(_serialized_additional_media_type(value))
|
|
833
|
+
or is_image_data_uri(value.get("uri"))
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def _serialized_additional_media_type(value: Mapping[Any, Any]) -> Any:
|
|
838
|
+
additional_properties = value.get("additional_properties")
|
|
839
|
+
if not isinstance(additional_properties, dict):
|
|
840
|
+
return None
|
|
841
|
+
return additional_properties.get("media_type")
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def _serialized_arguments_are_text_budgeted(value: Mapping[Any, Any]) -> bool:
|
|
845
|
+
return value.get("type") in _TEXT_BUDGETED_ARGUMENT_CONTENT_TYPES
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def _serialize_message(message: Message) -> str:
|
|
849
|
+
"""Produce stable estimator serialization with sorted keys.
|
|
850
|
+
|
|
851
|
+
Keep non-ASCII text literal for mixed-language token counting.
|
|
852
|
+
"""
|
|
853
|
+
serialized_contents = [_serialize_content(content) for content in message.contents]
|
|
854
|
+
payload = {
|
|
855
|
+
"role": message.role,
|
|
856
|
+
"message_id": message.message_id,
|
|
857
|
+
"contents": serialized_contents,
|
|
858
|
+
}
|
|
859
|
+
return json.dumps(payload, ensure_ascii=False, sort_keys=True, default=str)
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _message_token_estimate(message: Message, tokenizer: TokenizerProtocol) -> int:
|
|
863
|
+
"""Return serialized text tokens plus image-content estimates."""
|
|
864
|
+
image_tokens = estimate_message_image_tokens(message)
|
|
865
|
+
return tokenizer.count_tokens(_serialize_message(message)) + image_tokens
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def estimate_message_tokens(message: Message, tokenizer: TokenizerProtocol) -> int:
|
|
869
|
+
"""Estimate one message without mutating message or content metadata.
|
|
870
|
+
|
|
871
|
+
The annotation path opportunistically caches decoded image dimensions on
|
|
872
|
+
content objects. Scoped Phase-4 slices must remain strictly read-only
|
|
873
|
+
during admission and clipping calculations, so they use this variant.
|
|
874
|
+
"""
|
|
875
|
+
image_tokens = sum(
|
|
876
|
+
_estimate_image_tokens_in_tree(content, set(), cache_dimensions=False) for content in message.contents
|
|
877
|
+
)
|
|
878
|
+
return tokenizer.count_tokens(_serialize_message(message)) + image_tokens
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def estimate_message_image_tokens(message: Message) -> int:
|
|
882
|
+
"""Estimate image tokens for direct and nested image contents."""
|
|
883
|
+
total = 0
|
|
884
|
+
stack: set[int] = set()
|
|
885
|
+
for content in message.contents:
|
|
886
|
+
total += _estimate_image_tokens_in_tree(content, stack, cache_dimensions=True)
|
|
887
|
+
return total
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def estimate_toolresult_image_tokens(message: Message) -> int:
|
|
891
|
+
"""Estimate image tokens for function_result.items omitted from serialization."""
|
|
892
|
+
total = 0
|
|
893
|
+
for content in message.contents:
|
|
894
|
+
if content.type != "function_result" or not content.items:
|
|
895
|
+
continue
|
|
896
|
+
for item in content.items:
|
|
897
|
+
if is_image_content(item):
|
|
898
|
+
total += _estimate_image_tokens(item)
|
|
899
|
+
return total
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def _estimate_image_tokens_in_tree(value: Any, stack: set[int], *, cache_dimensions: bool) -> int:
|
|
903
|
+
if isinstance(value, Content):
|
|
904
|
+
content_id = id(value)
|
|
905
|
+
if content_id in stack:
|
|
906
|
+
return 0
|
|
907
|
+
stack.add(content_id)
|
|
908
|
+
try:
|
|
909
|
+
total = _estimate_image_tokens(value, cache_dimensions=cache_dimensions) if is_image_content(value) else 0
|
|
910
|
+
for child_value in _content_child_values(value):
|
|
911
|
+
total += _estimate_image_tokens_in_tree(child_value, stack, cache_dimensions=cache_dimensions)
|
|
912
|
+
return total
|
|
913
|
+
finally:
|
|
914
|
+
stack.remove(content_id)
|
|
915
|
+
if isinstance(value, Mapping):
|
|
916
|
+
mapping_id = id(value)
|
|
917
|
+
if mapping_id in stack:
|
|
918
|
+
return 0
|
|
919
|
+
stack.add(mapping_id)
|
|
920
|
+
try:
|
|
921
|
+
if _serialized_dict_is_image_content(value):
|
|
922
|
+
return _estimate_serialized_image_tokens(value, cache_dimensions=cache_dimensions)
|
|
923
|
+
total = 0
|
|
924
|
+
for key, nested in value.items():
|
|
925
|
+
if key == "arguments" and _serialized_arguments_are_text_budgeted(value):
|
|
926
|
+
continue
|
|
927
|
+
total += _estimate_image_tokens_in_tree(nested, stack, cache_dimensions=cache_dimensions)
|
|
928
|
+
return total
|
|
929
|
+
finally:
|
|
930
|
+
stack.remove(mapping_id)
|
|
931
|
+
if isinstance(value, Sequence) and not isinstance(value, str | bytes | bytearray):
|
|
932
|
+
sequence_id = id(value)
|
|
933
|
+
if sequence_id in stack:
|
|
934
|
+
return 0
|
|
935
|
+
stack.add(sequence_id)
|
|
936
|
+
try:
|
|
937
|
+
total = 0
|
|
938
|
+
for nested in value:
|
|
939
|
+
total += _estimate_image_tokens_in_tree(nested, stack, cache_dimensions=cache_dimensions)
|
|
940
|
+
return total
|
|
941
|
+
finally:
|
|
942
|
+
stack.remove(sequence_id)
|
|
943
|
+
return 0
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
def _content_child_values(content: Content) -> Iterable[Any]:
|
|
947
|
+
# Keep this aligned with the serialized fields walked by _redact_serialized_image_uris.
|
|
948
|
+
return (
|
|
949
|
+
content.items,
|
|
950
|
+
content.inputs,
|
|
951
|
+
content.outputs,
|
|
952
|
+
content.output,
|
|
953
|
+
content.result,
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def _estimate_image_tokens(content: Content, *, cache_dimensions: bool = True) -> int:
|
|
958
|
+
width = _positive_int(content.additional_properties.get("width"))
|
|
959
|
+
height = _positive_int(content.additional_properties.get("height"))
|
|
960
|
+
if width is None or height is None:
|
|
961
|
+
inferred_dimensions = _image_dimensions_from_data_uri(content.uri)
|
|
962
|
+
if inferred_dimensions is not None:
|
|
963
|
+
width, height = inferred_dimensions
|
|
964
|
+
if cache_dimensions:
|
|
965
|
+
_cache_content_image_dimensions(content, width, height)
|
|
966
|
+
if width is None or height is None:
|
|
967
|
+
return _IMAGE_FALLBACK_TOKENS
|
|
968
|
+
return _image_tokens_from_dims(width, height)
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def _estimate_serialized_image_tokens(content: Mapping[Any, Any], *, cache_dimensions: bool = True) -> int:
|
|
972
|
+
width = _positive_int(content.get("width"))
|
|
973
|
+
height = _positive_int(content.get("height"))
|
|
974
|
+
additional_properties = content.get("additional_properties")
|
|
975
|
+
if isinstance(additional_properties, Mapping):
|
|
976
|
+
width = width or _positive_int(additional_properties.get("width"))
|
|
977
|
+
height = height or _positive_int(additional_properties.get("height"))
|
|
978
|
+
if width is None or height is None:
|
|
979
|
+
inferred_dimensions = _image_dimensions_from_data_uri(content.get("uri"))
|
|
980
|
+
if inferred_dimensions is not None:
|
|
981
|
+
width, height = inferred_dimensions
|
|
982
|
+
if cache_dimensions and isinstance(content, dict):
|
|
983
|
+
_cache_serialized_image_dimensions(content, width, height)
|
|
984
|
+
if width is None or height is None:
|
|
985
|
+
return _IMAGE_FALLBACK_TOKENS
|
|
986
|
+
return _image_tokens_from_dims(width, height)
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def _image_tokens_from_dims(width: int, height: int) -> int:
|
|
990
|
+
tiles = max(1, math.ceil(width / _IMAGE_TILE_SIZE)) * max(1, math.ceil(height / _IMAGE_TILE_SIZE))
|
|
991
|
+
return _IMAGE_BASE_TOKENS + (_IMAGE_TILE_TOKENS * tiles)
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def _cache_content_image_dimensions(content: Content, width: int, height: int) -> None:
|
|
995
|
+
content.additional_properties["width"] = width
|
|
996
|
+
content.additional_properties["height"] = height
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def _cache_serialized_image_dimensions(content: dict[Any, Any], width: int, height: int) -> None:
|
|
1000
|
+
additional_properties = content.get("additional_properties")
|
|
1001
|
+
if not isinstance(additional_properties, dict):
|
|
1002
|
+
additional_properties = {}
|
|
1003
|
+
content["additional_properties"] = additional_properties
|
|
1004
|
+
additional_properties["width"] = width
|
|
1005
|
+
additional_properties["height"] = height
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def _image_dimensions_from_data_uri(uri: Any) -> tuple[int, int] | None:
|
|
1009
|
+
if not isinstance(uri, str):
|
|
1010
|
+
return None
|
|
1011
|
+
normalized_uri = uri.lower()
|
|
1012
|
+
marker = ";base64,"
|
|
1013
|
+
marker_index = normalized_uri.find(marker)
|
|
1014
|
+
if not normalized_uri.startswith("data:") or marker_index < 0:
|
|
1015
|
+
return None
|
|
1016
|
+
encoded = uri[marker_index + len(marker) :]
|
|
1017
|
+
try:
|
|
1018
|
+
data = base64.b64decode(encoded, validate=False)
|
|
1019
|
+
except binascii.Error, ValueError:
|
|
1020
|
+
return None
|
|
1021
|
+
try:
|
|
1022
|
+
return inspect_image_dimensions(data)
|
|
1023
|
+
except Exception:
|
|
1024
|
+
return None
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def _positive_int(value: Any) -> int | None:
|
|
1028
|
+
if isinstance(value, bool):
|
|
1029
|
+
return None
|
|
1030
|
+
if isinstance(value, int) and value > 0:
|
|
1031
|
+
return value
|
|
1032
|
+
if isinstance(value, str) and value.isdecimal():
|
|
1033
|
+
parsed = int(value)
|
|
1034
|
+
return parsed if parsed > 0 else None
|
|
1035
|
+
return None
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def annotate_token_counts(
|
|
1039
|
+
messages: list[Message],
|
|
1040
|
+
*,
|
|
1041
|
+
tokenizer: TokenizerProtocol,
|
|
1042
|
+
from_index: int | None = None,
|
|
1043
|
+
force_retokenize: bool = False,
|
|
1044
|
+
) -> None:
|
|
1045
|
+
"""Annotate token-count metadata, incrementally by default."""
|
|
1046
|
+
if not messages:
|
|
1047
|
+
return
|
|
1048
|
+
|
|
1049
|
+
# Token counts are stored inside group annotations.
|
|
1050
|
+
annotate_message_groups(messages, from_index=from_index)
|
|
1051
|
+
|
|
1052
|
+
if force_retokenize:
|
|
1053
|
+
start_index = 0
|
|
1054
|
+
elif from_index is not None:
|
|
1055
|
+
start_index = max(0, min(from_index, len(messages) - 1))
|
|
1056
|
+
else:
|
|
1057
|
+
first_untokenized_index = _first_untokenized_index(messages)
|
|
1058
|
+
if first_untokenized_index is None:
|
|
1059
|
+
return
|
|
1060
|
+
start_index = first_untokenized_index
|
|
1061
|
+
|
|
1062
|
+
for message in messages[start_index:]:
|
|
1063
|
+
_write_token_count(message, _message_token_estimate(message, tokenizer))
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
def included_messages(messages: list[Message]) -> list[Message]:
|
|
1067
|
+
"""Return the messages not marked as excluded, preserving their order and identity."""
|
|
1068
|
+
return [message for message in messages if not message.additional_properties.get(EXCLUDED_KEY, False)]
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
def _carries_call_or_result(message: Message) -> bool:
|
|
1072
|
+
"""Whether the message participates in the group's call/result pairing.
|
|
1073
|
+
|
|
1074
|
+
Narration-only members (assistant text fused between a call and its
|
|
1075
|
+
results) are wire-legal on their own; only carriers must project as an
|
|
1076
|
+
all-or-nothing set. Classification is content-scoped regardless of the
|
|
1077
|
+
kernel role carrying it: provider serializers emit call/result blocks by
|
|
1078
|
+
content (a user-role function_result still becomes a paired result block
|
|
1079
|
+
on the wire), so pairing atomicity must cover every carrier the group
|
|
1080
|
+
spans even when the exchange grammar treats the message as transcript
|
|
1081
|
+
input for boundary purposes."""
|
|
1082
|
+
return any(
|
|
1083
|
+
content.type in TOOL_CALL_CONTENT_TYPES or content.type in TOOL_RESULT_CONTENT_TYPES
|
|
1084
|
+
for content in message.contents
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
@dataclass(frozen=True, slots=True)
|
|
1089
|
+
class _PartialToolCallGroup:
|
|
1090
|
+
"""One positional tool-call annotation occurrence with a partial exclusion.
|
|
1091
|
+
|
|
1092
|
+
``degrade_indices`` are the absolute indices projection must exclude to
|
|
1093
|
+
restore atomicity: the group's call/result carriers plus its
|
|
1094
|
+
reasoning-bearing members (reasoning riders never float free of their
|
|
1095
|
+
calls). Plain narration members stay included."""
|
|
1096
|
+
|
|
1097
|
+
position: int
|
|
1098
|
+
degrade_indices: tuple[int, ...]
|
|
1099
|
+
group_id: str
|
|
1100
|
+
member_snapshot: tuple[weakref.ref[Message], ...]
|
|
1101
|
+
"""Weak snapshot of the member Messages at discovery time.
|
|
1102
|
+
|
|
1103
|
+
Compared across the strategy await by :func:`_same_live_members` —
|
|
1104
|
+
order-insensitive, and dead members never compare equal, so a freed
|
|
1105
|
+
group can never vouch for a same-shaped newcomer at a recycled
|
|
1106
|
+
address.
|
|
1107
|
+
"""
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def _same_live_members(
|
|
1111
|
+
left: tuple[weakref.ref[Message], ...],
|
|
1112
|
+
right: tuple[weakref.ref[Message], ...],
|
|
1113
|
+
) -> bool:
|
|
1114
|
+
"""Whether two member snapshots name the same set of LIVE referents.
|
|
1115
|
+
|
|
1116
|
+
Order-insensitive by design: today's semantics accept a strategy that
|
|
1117
|
+
merely reorders a partial group's messages, and an order-sensitive
|
|
1118
|
+
comparison would reclassify that as a newly introduced partial group.
|
|
1119
|
+
Any dead member fails the comparison outright.
|
|
1120
|
+
"""
|
|
1121
|
+
left_members = [ref() for ref in left]
|
|
1122
|
+
right_members = [ref() for ref in right]
|
|
1123
|
+
if any(member is None for member in left_members) or any(member is None for member in right_members):
|
|
1124
|
+
return False
|
|
1125
|
+
return {id(member) for member in left_members} == {id(member) for member in right_members}
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
def group_annotation_signature(message: Message) -> tuple[str, GroupKind, int, bool] | None:
|
|
1129
|
+
"""Return the validated fields that delimit one contiguous annotation occurrence."""
|
|
1130
|
+
annotation = _read_group_annotation(message)
|
|
1131
|
+
if annotation is None:
|
|
1132
|
+
return None
|
|
1133
|
+
return (
|
|
1134
|
+
cast("str", annotation[GROUP_ID_KEY]),
|
|
1135
|
+
cast("GroupKind", annotation[GROUP_KIND_KEY]),
|
|
1136
|
+
cast("int", annotation[GROUP_INDEX_KEY]),
|
|
1137
|
+
cast("bool", annotation[GROUP_HAS_REASONING_KEY]),
|
|
1138
|
+
)
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def _partial_tool_call_groups(messages: Sequence[Message]) -> list[_PartialToolCallGroup]:
|
|
1142
|
+
"""Find partially excluded tool-call groups without joining disjoint equal IDs."""
|
|
1143
|
+
partial_groups: list[_PartialToolCallGroup] = []
|
|
1144
|
+
index = 0
|
|
1145
|
+
position = 0
|
|
1146
|
+
while index < len(messages):
|
|
1147
|
+
signature = group_annotation_signature(messages[index])
|
|
1148
|
+
if signature is None:
|
|
1149
|
+
index += 1
|
|
1150
|
+
continue
|
|
1151
|
+
|
|
1152
|
+
# Adjacent but distinct occurrences can persist byte-identical
|
|
1153
|
+
# signatures; a tool-call group never resumes its assistant block after
|
|
1154
|
+
# its results, so an assistant message following the group's results
|
|
1155
|
+
# starts a new occurrence even under an unchanged signature — unless it
|
|
1156
|
+
# only carries results for the block. Results ride tool-role messages
|
|
1157
|
+
# or result-only assistant messages (hosted MCP), matching the group
|
|
1158
|
+
# annotator's own follower rule; other roles carrying result contents
|
|
1159
|
+
# are transcript input, never the group's output run.
|
|
1160
|
+
seen_results = messages[index].role == "tool" or is_result_only_message(messages[index], _LIVE_ACCESSOR)
|
|
1161
|
+
end_index = index + 1
|
|
1162
|
+
while end_index < len(messages) and group_annotation_signature(messages[end_index]) == signature:
|
|
1163
|
+
follower = messages[end_index]
|
|
1164
|
+
follower_is_result = follower.role == "tool" or is_result_only_message(follower, _LIVE_ACCESSOR)
|
|
1165
|
+
if seen_results and follower.role == "assistant" and not follower_is_result:
|
|
1166
|
+
break
|
|
1167
|
+
seen_results = seen_results or follower_is_result
|
|
1168
|
+
end_index += 1
|
|
1169
|
+
|
|
1170
|
+
group_id, kind, _group_index_value, _has_reasoning_value = signature
|
|
1171
|
+
if kind == "tool_call":
|
|
1172
|
+
# Atomic set: call/result carriers plus reasoning-bearing members
|
|
1173
|
+
# (a reasoning item must never outlive its exchange). Plain fused
|
|
1174
|
+
# narration is wire-legal alone and stays free in every group.
|
|
1175
|
+
atomic_indices = [
|
|
1176
|
+
i
|
|
1177
|
+
for i in range(index, end_index)
|
|
1178
|
+
if _carries_call_or_result(messages[i]) or _has_reasoning(messages[i])
|
|
1179
|
+
]
|
|
1180
|
+
excluded = [bool(messages[i].additional_properties.get(EXCLUDED_KEY, False)) for i in atomic_indices]
|
|
1181
|
+
if any(excluded) and not all(excluded):
|
|
1182
|
+
members = messages[index:end_index]
|
|
1183
|
+
partial_groups.append(
|
|
1184
|
+
_PartialToolCallGroup(
|
|
1185
|
+
position=position,
|
|
1186
|
+
degrade_indices=tuple(atomic_indices),
|
|
1187
|
+
group_id=group_id,
|
|
1188
|
+
member_snapshot=tuple(weakref.ref(message) for message in members),
|
|
1189
|
+
)
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
position += 1
|
|
1193
|
+
index = end_index
|
|
1194
|
+
return partial_groups
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def _degrade_partial_tool_call_groups(
|
|
1198
|
+
messages: list[Message],
|
|
1199
|
+
partial_groups: Sequence[_PartialToolCallGroup],
|
|
1200
|
+
) -> None:
|
|
1201
|
+
"""Exclude each partial group's surviving atomic members."""
|
|
1202
|
+
if not partial_groups:
|
|
1203
|
+
return
|
|
1204
|
+
|
|
1205
|
+
for group in partial_groups:
|
|
1206
|
+
for message_index in group.degrade_indices:
|
|
1207
|
+
message = messages[message_index]
|
|
1208
|
+
if not message.additional_properties.get(EXCLUDED_KEY, False):
|
|
1209
|
+
set_excluded(
|
|
1210
|
+
message,
|
|
1211
|
+
excluded=True,
|
|
1212
|
+
reason=_PROJECTION_ATOMICITY_EXCLUDE_REASON,
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
logger.warning(
|
|
1216
|
+
"Degraded partially projected tool-call groups: positions=%s group_ids=%s",
|
|
1217
|
+
[group.position for group in partial_groups],
|
|
1218
|
+
[group.group_id for group in partial_groups],
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
def project_included_messages(messages: list[Message]) -> list[Message]:
|
|
1223
|
+
"""Return model-input messages after atomically degrading partial tool-call groups."""
|
|
1224
|
+
_degrade_partial_tool_call_groups(
|
|
1225
|
+
messages,
|
|
1226
|
+
_partial_tool_call_groups(messages),
|
|
1227
|
+
)
|
|
1228
|
+
return included_messages(messages)
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
def included_token_count(messages: list[Message]) -> int:
|
|
1232
|
+
"""Sum included token counts; untokenized messages count as 0."""
|
|
1233
|
+
total = 0
|
|
1234
|
+
for message in included_messages(messages):
|
|
1235
|
+
token_count = _token_count(message)
|
|
1236
|
+
if token_count is not None:
|
|
1237
|
+
total += token_count
|
|
1238
|
+
return total
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
def set_excluded(message: Message, *, excluded: bool, reason: str | None = None) -> bool:
|
|
1242
|
+
"""Set the exclusion flag and return whether it changed.
|
|
1243
|
+
|
|
1244
|
+
A ``None`` reason leaves any previously recorded reason in place.
|
|
1245
|
+
"""
|
|
1246
|
+
changed = bool(message.additional_properties.get(EXCLUDED_KEY, False)) != excluded
|
|
1247
|
+
if changed:
|
|
1248
|
+
message.additional_properties[EXCLUDED_KEY] = excluded
|
|
1249
|
+
if reason is not None:
|
|
1250
|
+
message.additional_properties[EXCLUDE_REASON_KEY] = reason
|
|
1251
|
+
return changed
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
def exclude_group_ids(messages: list[Message], group_ids: set[str], *, reason: str) -> bool:
|
|
1255
|
+
"""Exclude all messages whose annotation belongs to one of ``group_ids``."""
|
|
1256
|
+
changed = False
|
|
1257
|
+
for message in messages:
|
|
1258
|
+
group_id = _group_id(message)
|
|
1259
|
+
if group_id is not None and group_id in group_ids:
|
|
1260
|
+
changed = set_excluded(message, excluded=True, reason=reason) or changed
|
|
1261
|
+
return changed
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
async def apply_compaction(
|
|
1265
|
+
messages: list[Message],
|
|
1266
|
+
*,
|
|
1267
|
+
strategy: CompactionStrategy | None,
|
|
1268
|
+
tokenizer: TokenizerProtocol | None = None,
|
|
1269
|
+
context: CompactionCallContext | None = None,
|
|
1270
|
+
strict_projection_atomicity: bool = False,
|
|
1271
|
+
) -> list[Message]:
|
|
1272
|
+
"""Apply configured compaction and return projected model-input messages.
|
|
1273
|
+
|
|
1274
|
+
The strategy is always called with both positional arguments
|
|
1275
|
+
(``strategy(messages, context)``); see the :class:`CompactionStrategy`
|
|
1276
|
+
breaking-change note for pre-context strategy signatures. Strict
|
|
1277
|
+
projection atomicity runs the strategy on a transactional copy and
|
|
1278
|
+
commits only when the strategy did not introduce a partial tool-call
|
|
1279
|
+
group. Partial groups already present before the strategy are degraded
|
|
1280
|
+
normally.
|
|
1281
|
+
"""
|
|
1282
|
+
if strategy is None:
|
|
1283
|
+
return project_included_messages(messages)
|
|
1284
|
+
|
|
1285
|
+
working_messages = deepcopy(messages) if strict_projection_atomicity else messages
|
|
1286
|
+
annotate_message_groups(working_messages)
|
|
1287
|
+
if tokenizer is not None:
|
|
1288
|
+
annotate_token_counts(working_messages, tokenizer=tokenizer)
|
|
1289
|
+
preexisting_partial_snapshots = (
|
|
1290
|
+
[group.member_snapshot for group in _partial_tool_call_groups(working_messages)]
|
|
1291
|
+
if strict_projection_atomicity
|
|
1292
|
+
else []
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
await strategy(working_messages, context)
|
|
1296
|
+
|
|
1297
|
+
partial_groups = _partial_tool_call_groups(working_messages)
|
|
1298
|
+
if strict_projection_atomicity:
|
|
1299
|
+
introduced_partial_groups = [
|
|
1300
|
+
group
|
|
1301
|
+
for group in partial_groups
|
|
1302
|
+
if not any(
|
|
1303
|
+
_same_live_members(group.member_snapshot, snapshot) for snapshot in preexisting_partial_snapshots
|
|
1304
|
+
)
|
|
1305
|
+
]
|
|
1306
|
+
if introduced_partial_groups:
|
|
1307
|
+
raise CompactionProjectionAtomicityError(
|
|
1308
|
+
"Strict compaction rejected partially projected tool-call groups "
|
|
1309
|
+
f"at positions {[group.position for group in introduced_partial_groups]}"
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
projected = project_included_messages(working_messages)
|
|
1313
|
+
if strict_projection_atomicity:
|
|
1314
|
+
messages[:] = working_messages
|
|
1315
|
+
return projected
|