@artooi/agents-web-components 0.0.0-stage → 0.1.1
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.
- package/CHANGELOG.md +77 -0
- package/LICENSE +21 -0
- package/README.md +4397 -2
- package/dist/agents-web-components.bundle.js +5708 -0
- package/dist/agents-web-components.bundle.js.map +7 -0
- package/dist/bundle.d.ts +3 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/chat.d.ts +3 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +16373 -0
- package/dist/chat.js.map +7 -0
- package/dist/chunk-ARNLFGCC.js +7715 -0
- package/dist/chunk-ARNLFGCC.js.map +7 -0
- package/dist/constants.d.ts +575 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/core/activity_registration.d.ts +19 -0
- package/dist/core/activity_registration.d.ts.map +1 -0
- package/dist/core/activity_registry.d.ts +46 -0
- package/dist/core/activity_registry.d.ts.map +1 -0
- package/dist/core/activity_renderer.d.ts +20 -0
- package/dist/core/activity_renderer.d.ts.map +1 -0
- package/dist/core/ag_ui_chat.d.ts +836 -0
- package/dist/core/ag_ui_chat.d.ts.map +1 -0
- package/dist/core/agui_client.d.ts +370 -0
- package/dist/core/agui_client.d.ts.map +1 -0
- package/dist/core/attachment.d.ts +42 -0
- package/dist/core/attachment.d.ts.map +1 -0
- package/dist/core/chat_registry.d.ts +23 -0
- package/dist/core/chat_registry.d.ts.map +1 -0
- package/dist/core/client_seed.d.ts +27 -0
- package/dist/core/client_seed.d.ts.map +1 -0
- package/dist/core/conversation_store.d.ts +195 -0
- package/dist/core/conversation_store.d.ts.map +1 -0
- package/dist/core/create_http_agent.d.ts +57 -0
- package/dist/core/create_http_agent.d.ts.map +1 -0
- package/dist/core/define_chat.d.ts +13 -0
- package/dist/core/define_chat.d.ts.map +1 -0
- package/dist/core/events/attachments_detail.d.ts +9 -0
- package/dist/core/events/attachments_detail.d.ts.map +1 -0
- package/dist/core/events/bar_height_detail.d.ts +6 -0
- package/dist/core/events/bar_height_detail.d.ts.map +1 -0
- package/dist/core/events/custom_agent_detail.d.ts +8 -0
- package/dist/core/events/custom_agent_detail.d.ts.map +1 -0
- package/dist/core/events/dock_width_detail.d.ts +8 -0
- package/dist/core/events/dock_width_detail.d.ts.map +1 -0
- package/dist/core/events/feedback_detail.d.ts +28 -0
- package/dist/core/events/feedback_detail.d.ts.map +1 -0
- package/dist/core/events/invalidate_detail.d.ts +16 -0
- package/dist/core/events/invalidate_detail.d.ts.map +1 -0
- package/dist/core/events/maximize_detail.d.ts +5 -0
- package/dist/core/events/maximize_detail.d.ts.map +1 -0
- package/dist/core/events/run_finished_detail.d.ts +36 -0
- package/dist/core/events/run_finished_detail.d.ts.map +1 -0
- package/dist/core/events/state_detail.d.ts +5 -0
- package/dist/core/events/state_detail.d.ts.map +1 -0
- package/dist/core/events/submit_detail.d.ts +8 -0
- package/dist/core/events/submit_detail.d.ts.map +1 -0
- package/dist/core/events/thread_detail.d.ts +27 -0
- package/dist/core/events/thread_detail.d.ts.map +1 -0
- package/dist/core/events/thread_title_detail.d.ts +8 -0
- package/dist/core/events/thread_title_detail.d.ts.map +1 -0
- package/dist/core/events/toggle_detail.d.ts +5 -0
- package/dist/core/events/toggle_detail.d.ts.map +1 -0
- package/dist/core/events/tool_run.d.ts +11 -0
- package/dist/core/events/tool_run.d.ts.map +1 -0
- package/dist/core/events/unread_detail.d.ts +11 -0
- package/dist/core/events/unread_detail.d.ts.map +1 -0
- package/dist/core/message_role.d.ts +4 -0
- package/dist/core/message_role.d.ts.map +1 -0
- package/dist/core/read_flag.d.ts +11 -0
- package/dist/core/read_flag.d.ts.map +1 -0
- package/dist/core/read_json_attribute.d.ts +15 -0
- package/dist/core/read_json_attribute.d.ts.map +1 -0
- package/dist/core/read_max_tool_rounds.d.ts +9 -0
- package/dist/core/read_max_tool_rounds.d.ts.map +1 -0
- package/dist/core/remote_conversation_store.d.ts +71 -0
- package/dist/core/remote_conversation_store.d.ts.map +1 -0
- package/dist/core/run_handlers.d.ts +85 -0
- package/dist/core/run_handlers.d.ts.map +1 -0
- package/dist/core/run_index.d.ts +58 -0
- package/dist/core/run_index.d.ts.map +1 -0
- package/dist/core/storage_scope.d.ts +118 -0
- package/dist/core/storage_scope.d.ts.map +1 -0
- package/dist/core/tool_outcome.d.ts +27 -0
- package/dist/core/tool_outcome.d.ts.map +1 -0
- package/dist/core/transcribe_audio.d.ts +32 -0
- package/dist/core/transcribe_audio.d.ts.map +1 -0
- package/dist/core/upload_attachment.d.ts +44 -0
- package/dist/core/upload_attachment.d.ts.map +1 -0
- package/dist/core/utils.d.ts +117 -0
- package/dist/core/utils.d.ts.map +1 -0
- package/dist/dom/animations.d.ts +122 -0
- package/dist/dom/animations.d.ts.map +1 -0
- package/dist/dom/dom_driver.d.ts +28 -0
- package/dist/dom/dom_driver.d.ts.map +1 -0
- package/dist/dom/highlight_overlay.d.ts +39 -0
- package/dist/dom/highlight_overlay.d.ts.map +1 -0
- package/dist/dom/native_setter.d.ts +5 -0
- package/dist/dom/native_setter.d.ts.map +1 -0
- package/dist/dom/ring_color.d.ts +16 -0
- package/dist/dom/ring_color.d.ts.map +1 -0
- package/dist/dom/utils.d.ts +3 -0
- package/dist/dom/utils.d.ts.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +223 -0
- package/dist/index.js.map +7 -0
- package/dist/skills/fill_template.d.ts +16 -0
- package/dist/skills/fill_template.d.ts.map +1 -0
- package/dist/skills/parse_skills.d.ts +8 -0
- package/dist/skills/parse_skills.d.ts.map +1 -0
- package/dist/skills/skill.d.ts +33 -0
- package/dist/skills/skill.d.ts.map +1 -0
- package/dist/skills/skill_catalog.d.ts +69 -0
- package/dist/skills/skill_catalog.d.ts.map +1 -0
- package/dist/skills/skill_name_from.d.ts +12 -0
- package/dist/skills/skill_name_from.d.ts.map +1 -0
- package/dist/tools/chat_surface_tools.d.ts +77 -0
- package/dist/tools/chat_surface_tools.d.ts.map +1 -0
- package/dist/tools/client_tool_registry.d.ts +58 -0
- package/dist/tools/client_tool_registry.d.ts.map +1 -0
- package/dist/tools/is_destructive.d.ts +14 -0
- package/dist/tools/is_destructive.d.ts.map +1 -0
- package/dist/tools/is_navigates.d.ts +9 -0
- package/dist/tools/is_navigates.d.ts.map +1 -0
- package/dist/tools/page_action_tools.d.ts +30 -0
- package/dist/tools/page_action_tools.d.ts.map +1 -0
- package/dist/tools/page_map.d.ts +16 -0
- package/dist/tools/page_map.d.ts.map +1 -0
- package/dist/tools/page_state.d.ts +27 -0
- package/dist/tools/page_state.d.ts.map +1 -0
- package/dist/tools/parse_tool_catalog.d.ts +21 -0
- package/dist/tools/parse_tool_catalog.d.ts.map +1 -0
- package/dist/tools/route_map.d.ts +37 -0
- package/dist/tools/route_map.d.ts.map +1 -0
- package/dist/tools/tool_catalog.d.ts +86 -0
- package/dist/tools/tool_catalog.d.ts.map +1 -0
- package/dist/tools/tool_dispatch.d.ts +82 -0
- package/dist/tools/tool_dispatch.d.ts.map +1 -0
- package/dist/ui/charts/chart_block.d.ts +57 -0
- package/dist/ui/charts/chart_block.d.ts.map +1 -0
- package/dist/ui/charts/chart_spec_from.d.ts +12 -0
- package/dist/ui/charts/chart_spec_from.d.ts.map +1 -0
- package/dist/ui/charts/chart_styles.d.ts +2 -0
- package/dist/ui/charts/chart_styles.d.ts.map +1 -0
- package/dist/ui/charts/chart_tool.d.ts +15 -0
- package/dist/ui/charts/chart_tool.d.ts.map +1 -0
- package/dist/ui/composer/attachment_styles.d.ts +2 -0
- package/dist/ui/composer/attachment_styles.d.ts.map +1 -0
- package/dist/ui/composer/attachment_tray.d.ts +53 -0
- package/dist/ui/composer/attachment_tray.d.ts.map +1 -0
- package/dist/ui/composer/auto_grow.d.ts +10 -0
- package/dist/ui/composer/auto_grow.d.ts.map +1 -0
- package/dist/ui/composer/build_composer.d.ts +32 -0
- package/dist/ui/composer/build_composer.d.ts.map +1 -0
- package/dist/ui/composer/composer_attachments.d.ts +57 -0
- package/dist/ui/composer/composer_attachments.d.ts.map +1 -0
- package/dist/ui/composer/composer_styles.d.ts +2 -0
- package/dist/ui/composer/composer_styles.d.ts.map +1 -0
- package/dist/ui/composer/composer_voice.d.ts +48 -0
- package/dist/ui/composer/composer_voice.d.ts.map +1 -0
- package/dist/ui/composer/skill_styles.d.ts +2 -0
- package/dist/ui/composer/skill_styles.d.ts.map +1 -0
- package/dist/ui/composer/skills_menu.d.ts +34 -0
- package/dist/ui/composer/skills_menu.d.ts.map +1 -0
- package/dist/ui/composer/voice_input.d.ts +46 -0
- package/dist/ui/composer/voice_input.d.ts.map +1 -0
- package/dist/ui/excerpts/attach_copy_buttons.d.ts +13 -0
- package/dist/ui/excerpts/attach_copy_buttons.d.ts.map +1 -0
- package/dist/ui/excerpts/copy_payload.d.ts +30 -0
- package/dist/ui/excerpts/copy_payload.d.ts.map +1 -0
- package/dist/ui/excerpts/page_quote_offer.d.ts +33 -0
- package/dist/ui/excerpts/page_quote_offer.d.ts.map +1 -0
- package/dist/ui/excerpts/place_offer.d.ts +9 -0
- package/dist/ui/excerpts/place_offer.d.ts.map +1 -0
- package/dist/ui/excerpts/quote_selection.d.ts +66 -0
- package/dist/ui/excerpts/quote_selection.d.ts.map +1 -0
- package/dist/ui/excerpts/transcript_quote_offer.d.ts +57 -0
- package/dist/ui/excerpts/transcript_quote_offer.d.ts.map +1 -0
- package/dist/ui/fill_ui_string.d.ts +32 -0
- package/dist/ui/fill_ui_string.d.ts.map +1 -0
- package/dist/ui/history/checkpoint_menu.d.ts +41 -0
- package/dist/ui/history/checkpoint_menu.d.ts.map +1 -0
- package/dist/ui/history/conversation_history.d.ts +317 -0
- package/dist/ui/history/conversation_history.d.ts.map +1 -0
- package/dist/ui/history/history_styles.d.ts +2 -0
- package/dist/ui/history/history_styles.d.ts.map +1 -0
- package/dist/ui/history/relative_time.d.ts +23 -0
- package/dist/ui/history/relative_time.d.ts.map +1 -0
- package/dist/ui/history/thread_drawer.d.ts +64 -0
- package/dist/ui/history/thread_drawer.d.ts.map +1 -0
- package/dist/ui/history/utils.d.ts +2 -0
- package/dist/ui/history/utils.d.ts.map +1 -0
- package/dist/ui/interrupts/approval_card.d.ts +64 -0
- package/dist/ui/interrupts/approval_card.d.ts.map +1 -0
- package/dist/ui/interrupts/approval_styles.d.ts +2 -0
- package/dist/ui/interrupts/approval_styles.d.ts.map +1 -0
- package/dist/ui/interrupts/confirmation_card.d.ts +51 -0
- package/dist/ui/interrupts/confirmation_card.d.ts.map +1 -0
- package/dist/ui/interrupts/confirmation_styles.d.ts +2 -0
- package/dist/ui/interrupts/confirmation_styles.d.ts.map +1 -0
- package/dist/ui/interrupts/pending_decision.d.ts +22 -0
- package/dist/ui/interrupts/pending_decision.d.ts.map +1 -0
- package/dist/ui/interrupts/question_card.d.ts +49 -0
- package/dist/ui/interrupts/question_card.d.ts.map +1 -0
- package/dist/ui/interrupts/question_styles.d.ts +2 -0
- package/dist/ui/interrupts/question_styles.d.ts.map +1 -0
- package/dist/ui/place/place.d.ts +17 -0
- package/dist/ui/place/place.d.ts.map +1 -0
- package/dist/ui/place/place_chip.d.ts +26 -0
- package/dist/ui/place/place_chip.d.ts.map +1 -0
- package/dist/ui/place/place_from_route.d.ts +9 -0
- package/dist/ui/place/place_from_route.d.ts.map +1 -0
- package/dist/ui/place/place_styles.d.ts +2 -0
- package/dist/ui/place/place_styles.d.ts.map +1 -0
- package/dist/ui/placement/clamp_launcher.d.ts +20 -0
- package/dist/ui/placement/clamp_launcher.d.ts.map +1 -0
- package/dist/ui/placement/clamp_panel.d.ts +13 -0
- package/dist/ui/placement/clamp_panel.d.ts.map +1 -0
- package/dist/ui/placement/collapse_styles.d.ts +2 -0
- package/dist/ui/placement/collapse_styles.d.ts.map +1 -0
- package/dist/ui/placement/dock_styles.d.ts +2 -0
- package/dist/ui/placement/dock_styles.d.ts.map +1 -0
- package/dist/ui/placement/embedded_styles.d.ts +2 -0
- package/dist/ui/placement/embedded_styles.d.ts.map +1 -0
- package/dist/ui/placement/launcher_drag.d.ts +49 -0
- package/dist/ui/placement/launcher_drag.d.ts.map +1 -0
- package/dist/ui/placement/launcher_placement.d.ts +77 -0
- package/dist/ui/placement/launcher_placement.d.ts.map +1 -0
- package/dist/ui/placement/launcher_styles.d.ts +2 -0
- package/dist/ui/placement/launcher_styles.d.ts.map +1 -0
- package/dist/ui/placement/page_styles.d.ts +2 -0
- package/dist/ui/placement/page_styles.d.ts.map +1 -0
- package/dist/ui/placement/panel_drag.d.ts +40 -0
- package/dist/ui/placement/panel_drag.d.ts.map +1 -0
- package/dist/ui/placement/panel_placement.d.ts +173 -0
- package/dist/ui/placement/panel_placement.d.ts.map +1 -0
- package/dist/ui/placement/place_widget.d.ts +31 -0
- package/dist/ui/placement/place_widget.d.ts.map +1 -0
- package/dist/ui/placement/preset_styles.d.ts +2 -0
- package/dist/ui/placement/preset_styles.d.ts.map +1 -0
- package/dist/ui/placement/resize_handle.d.ts +78 -0
- package/dist/ui/placement/resize_handle.d.ts.map +1 -0
- package/dist/ui/placement/resize_styles.d.ts +2 -0
- package/dist/ui/placement/resize_styles.d.ts.map +1 -0
- package/dist/ui/placement/sidebar_styles.d.ts +2 -0
- package/dist/ui/placement/sidebar_styles.d.ts.map +1 -0
- package/dist/ui/placement/small_viewport_styles.d.ts +2 -0
- package/dist/ui/placement/small_viewport_styles.d.ts.map +1 -0
- package/dist/ui/placement/surface_for.d.ts +93 -0
- package/dist/ui/placement/surface_for.d.ts.map +1 -0
- package/dist/ui/progress/prettify_tool_name.d.ts +11 -0
- package/dist/ui/progress/prettify_tool_name.d.ts.map +1 -0
- package/dist/ui/progress/run_announcer.d.ts +41 -0
- package/dist/ui/progress/run_announcer.d.ts.map +1 -0
- package/dist/ui/progress/run_notice.d.ts +21 -0
- package/dist/ui/progress/run_notice.d.ts.map +1 -0
- package/dist/ui/progress/run_notice_styles.d.ts +2 -0
- package/dist/ui/progress/run_notice_styles.d.ts.map +1 -0
- package/dist/ui/progress/subagent_panel.d.ts +101 -0
- package/dist/ui/progress/subagent_panel.d.ts.map +1 -0
- package/dist/ui/progress/subagent_progress.d.ts +57 -0
- package/dist/ui/progress/subagent_progress.d.ts.map +1 -0
- package/dist/ui/progress/subagent_update.d.ts +28 -0
- package/dist/ui/progress/subagent_update.d.ts.map +1 -0
- package/dist/ui/progress/thoughts_block.d.ts +57 -0
- package/dist/ui/progress/thoughts_block.d.ts.map +1 -0
- package/dist/ui/progress/thoughts_styles.d.ts +2 -0
- package/dist/ui/progress/thoughts_styles.d.ts.map +1 -0
- package/dist/ui/progress/tool_call_card.d.ts +167 -0
- package/dist/ui/progress/tool_call_card.d.ts.map +1 -0
- package/dist/ui/progress/tool_group_styles.d.ts +2 -0
- package/dist/ui/progress/tool_group_styles.d.ts.map +1 -0
- package/dist/ui/progress/tool_step_group.d.ts +50 -0
- package/dist/ui/progress/tool_step_group.d.ts.map +1 -0
- package/dist/ui/progress/tool_step_styles.d.ts +2 -0
- package/dist/ui/progress/tool_step_styles.d.ts.map +1 -0
- package/dist/ui/shell/adopt_styles.d.ts +28 -0
- package/dist/ui/shell/adopt_styles.d.ts.map +1 -0
- package/dist/ui/shell/build_header.d.ts +45 -0
- package/dist/ui/shell/build_header.d.ts.map +1 -0
- package/dist/ui/shell/build_launcher.d.ts +33 -0
- package/dist/ui/shell/build_launcher.d.ts.map +1 -0
- package/dist/ui/shell/collapse_glyph.d.ts +14 -0
- package/dist/ui/shell/collapse_glyph.d.ts.map +1 -0
- package/dist/ui/shell/frame_styles.d.ts +2 -0
- package/dist/ui/shell/frame_styles.d.ts.map +1 -0
- package/dist/ui/shell/glyph_slot.d.ts +7 -0
- package/dist/ui/shell/glyph_slot.d.ts.map +1 -0
- package/dist/ui/shell/header_button.d.ts +11 -0
- package/dist/ui/shell/header_button.d.ts.map +1 -0
- package/dist/ui/shell/icon_element.d.ts +12 -0
- package/dist/ui/shell/icon_element.d.ts.map +1 -0
- package/dist/ui/shell/is_unread_badge_enabled.d.ts +10 -0
- package/dist/ui/shell/is_unread_badge_enabled.d.ts.map +1 -0
- package/dist/ui/shell/read_launcher_icon_url.d.ts +8 -0
- package/dist/ui/shell/read_launcher_icon_url.d.ts.map +1 -0
- package/dist/ui/shell/theme_styles.d.ts +2 -0
- package/dist/ui/shell/theme_styles.d.ts.map +1 -0
- package/dist/ui/shell/token_styles.d.ts +2 -0
- package/dist/ui/shell/token_styles.d.ts.map +1 -0
- package/dist/ui/styles.d.ts +2 -0
- package/dist/ui/styles.d.ts.map +1 -0
- package/dist/ui/themes/classic_styles.d.ts +2 -0
- package/dist/ui/themes/classic_styles.d.ts.map +1 -0
- package/dist/ui/themes/classic_tokens.d.ts +2 -0
- package/dist/ui/themes/classic_tokens.d.ts.map +1 -0
- package/dist/ui/themes/code_tokens.d.ts +2 -0
- package/dist/ui/themes/code_tokens.d.ts.map +1 -0
- package/dist/ui/themes/ledger_styles.d.ts +2 -0
- package/dist/ui/themes/ledger_styles.d.ts.map +1 -0
- package/dist/ui/themes/ledger_tokens.d.ts +2 -0
- package/dist/ui/themes/ledger_tokens.d.ts.map +1 -0
- package/dist/ui/themes/modern_styles.d.ts +2 -0
- package/dist/ui/themes/modern_styles.d.ts.map +1 -0
- package/dist/ui/themes/quiet_styles.d.ts +2 -0
- package/dist/ui/themes/quiet_styles.d.ts.map +1 -0
- package/dist/ui/themes/scheme_styles.d.ts +2 -0
- package/dist/ui/themes/scheme_styles.d.ts.map +1 -0
- package/dist/ui/themes/soft_styles.d.ts +2 -0
- package/dist/ui/themes/soft_styles.d.ts.map +1 -0
- package/dist/ui/themes/soft_tokens.d.ts +2 -0
- package/dist/ui/themes/soft_tokens.d.ts.map +1 -0
- package/dist/ui/themes/theme_hosts.d.ts +15 -0
- package/dist/ui/themes/theme_hosts.d.ts.map +1 -0
- package/dist/ui/transcript/answer_actions.d.ts +50 -0
- package/dist/ui/transcript/answer_actions.d.ts.map +1 -0
- package/dist/ui/transcript/answer_stream.d.ts +56 -0
- package/dist/ui/transcript/answer_stream.d.ts.map +1 -0
- package/dist/ui/transcript/attachment_chip_styles.d.ts +2 -0
- package/dist/ui/transcript/attachment_chip_styles.d.ts.map +1 -0
- package/dist/ui/transcript/attachment_chips.d.ts +21 -0
- package/dist/ui/transcript/attachment_chips.d.ts.map +1 -0
- package/dist/ui/transcript/build_transcript.d.ts +21 -0
- package/dist/ui/transcript/build_transcript.d.ts.map +1 -0
- package/dist/ui/transcript/greeting_styles.d.ts +2 -0
- package/dist/ui/transcript/greeting_styles.d.ts.map +1 -0
- package/dist/ui/transcript/groups_tool_steps.d.ts +13 -0
- package/dist/ui/transcript/groups_tool_steps.d.ts.map +1 -0
- package/dist/ui/transcript/message_action_styles.d.ts +2 -0
- package/dist/ui/transcript/message_action_styles.d.ts.map +1 -0
- package/dist/ui/transcript/message_actions.d.ts +74 -0
- package/dist/ui/transcript/message_actions.d.ts.map +1 -0
- package/dist/ui/transcript/message_styles.d.ts +2 -0
- package/dist/ui/transcript/message_styles.d.ts.map +1 -0
- package/dist/ui/transcript/render_markdown.d.ts +40 -0
- package/dist/ui/transcript/render_markdown.d.ts.map +1 -0
- package/dist/ui/transcript/render_or_warn.d.ts +16 -0
- package/dist/ui/transcript/render_or_warn.d.ts.map +1 -0
- package/dist/ui/transcript/reveal_words.d.ts +8 -0
- package/dist/ui/transcript/reveal_words.d.ts.map +1 -0
- package/dist/ui/transcript/starter_chips.d.ts +21 -0
- package/dist/ui/transcript/starter_chips.d.ts.map +1 -0
- package/dist/ui/transcript/stick_to_bottom.d.ts +55 -0
- package/dist/ui/transcript/stick_to_bottom.d.ts.map +1 -0
- package/dist/ui/transcript/suggestion_chips.d.ts +29 -0
- package/dist/ui/transcript/suggestion_chips.d.ts.map +1 -0
- package/dist/ui/transcript/transcript.d.ts +196 -0
- package/dist/ui/transcript/transcript.d.ts.map +1 -0
- package/dist/ui/transcript/transcript_styles.d.ts +2 -0
- package/dist/ui/transcript/transcript_styles.d.ts.map +1 -0
- package/dist/ui/ui_strings.d.ts +352 -0
- package/dist/ui/ui_strings.d.ts.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/package.json +86 -3
- package/src/bundle.ts +2 -0
- package/src/chat.ts +2 -0
- package/src/constants.ts +650 -0
- package/src/core/activity_registration.ts +19 -0
- package/src/core/activity_registry.ts +133 -0
- package/src/core/activity_renderer.ts +19 -0
- package/src/core/ag_ui_chat.ts +3966 -0
- package/src/core/agui_client.ts +1028 -0
- package/src/core/attachment.ts +62 -0
- package/src/core/chat_registry.ts +53 -0
- package/src/core/client_seed.ts +27 -0
- package/src/core/conversation_store.ts +478 -0
- package/src/core/create_http_agent.ts +97 -0
- package/src/core/define_chat.ts +25 -0
- package/src/core/events/attachments_detail.ts +9 -0
- package/src/core/events/bar_height_detail.ts +5 -0
- package/src/core/events/custom_agent_detail.ts +7 -0
- package/src/core/events/dock_width_detail.ts +7 -0
- package/src/core/events/feedback_detail.ts +27 -0
- package/src/core/events/invalidate_detail.ts +15 -0
- package/src/core/events/maximize_detail.ts +4 -0
- package/src/core/events/run_finished_detail.ts +36 -0
- package/src/core/events/state_detail.ts +4 -0
- package/src/core/events/submit_detail.ts +8 -0
- package/src/core/events/thread_detail.ts +26 -0
- package/src/core/events/thread_title_detail.ts +7 -0
- package/src/core/events/toggle_detail.ts +4 -0
- package/src/core/events/tool_run.ts +10 -0
- package/src/core/events/unread_detail.ts +10 -0
- package/src/core/message_role.ts +4 -0
- package/src/core/read_flag.ts +13 -0
- package/src/core/read_json_attribute.ts +30 -0
- package/src/core/read_max_tool_rounds.ts +13 -0
- package/src/core/remote_conversation_store.ts +279 -0
- package/src/core/run_handlers.ts +524 -0
- package/src/core/run_index.ts +114 -0
- package/src/core/storage_scope.ts +291 -0
- package/src/core/tool_outcome.ts +39 -0
- package/src/core/transcribe_audio.ts +76 -0
- package/src/core/upload_attachment.ts +130 -0
- package/src/core/utils.ts +230 -0
- package/src/dom/animations.ts +341 -0
- package/src/dom/dom_driver.ts +91 -0
- package/src/dom/highlight_overlay.ts +204 -0
- package/src/dom/native_setter.ts +40 -0
- package/src/dom/ring_color.ts +206 -0
- package/src/dom/utils.ts +5 -0
- package/src/index.ts +220 -0
- package/src/skills/fill_template.ts +31 -0
- package/src/skills/parse_skills.ts +31 -0
- package/src/skills/skill.ts +32 -0
- package/src/skills/skill_catalog.ts +172 -0
- package/src/skills/skill_name_from.ts +19 -0
- package/src/tools/chat_surface_tools.ts +183 -0
- package/src/tools/client_tool_registry.ts +80 -0
- package/src/tools/is_destructive.ts +17 -0
- package/src/tools/is_navigates.ts +12 -0
- package/src/tools/page_action_tools.ts +123 -0
- package/src/tools/page_map.ts +26 -0
- package/src/tools/page_state.ts +57 -0
- package/src/tools/parse_tool_catalog.ts +41 -0
- package/src/tools/route_map.ts +132 -0
- package/src/tools/tool_catalog.ts +334 -0
- package/src/tools/tool_dispatch.ts +453 -0
- package/src/ui/charts/chart_block.ts +519 -0
- package/src/ui/charts/chart_spec_from.ts +125 -0
- package/src/ui/charts/chart_styles.ts +63 -0
- package/src/ui/charts/chart_tool.ts +72 -0
- package/src/ui/composer/attachment_styles.ts +71 -0
- package/src/ui/composer/attachment_tray.ts +295 -0
- package/src/ui/composer/auto_grow.ts +12 -0
- package/src/ui/composer/build_composer.ts +114 -0
- package/src/ui/composer/composer_attachments.ts +324 -0
- package/src/ui/composer/composer_styles.ts +177 -0
- package/src/ui/composer/composer_voice.ts +93 -0
- package/src/ui/composer/skill_styles.ts +120 -0
- package/src/ui/composer/skills_menu.ts +188 -0
- package/src/ui/composer/voice_input.ts +282 -0
- package/src/ui/excerpts/attach_copy_buttons.ts +73 -0
- package/src/ui/excerpts/copy_payload.ts +156 -0
- package/src/ui/excerpts/page_quote_offer.ts +203 -0
- package/src/ui/excerpts/place_offer.ts +24 -0
- package/src/ui/excerpts/quote_selection.ts +345 -0
- package/src/ui/excerpts/transcript_quote_offer.ts +175 -0
- package/src/ui/fill_ui_string.ts +41 -0
- package/src/ui/history/checkpoint_menu.ts +300 -0
- package/src/ui/history/conversation_history.ts +1169 -0
- package/src/ui/history/history_styles.ts +492 -0
- package/src/ui/history/relative_time.ts +50 -0
- package/src/ui/history/thread_drawer.ts +471 -0
- package/src/ui/history/utils.ts +11 -0
- package/src/ui/interrupts/approval_card.ts +202 -0
- package/src/ui/interrupts/approval_styles.ts +52 -0
- package/src/ui/interrupts/confirmation_card.ts +138 -0
- package/src/ui/interrupts/confirmation_styles.ts +114 -0
- package/src/ui/interrupts/pending_decision.ts +33 -0
- package/src/ui/interrupts/question_card.ts +213 -0
- package/src/ui/interrupts/question_styles.ts +73 -0
- package/src/ui/place/place.ts +16 -0
- package/src/ui/place/place_chip.ts +146 -0
- package/src/ui/place/place_from_route.ts +27 -0
- package/src/ui/place/place_styles.ts +108 -0
- package/src/ui/placement/clamp_launcher.ts +38 -0
- package/src/ui/placement/clamp_panel.ts +30 -0
- package/src/ui/placement/collapse_styles.ts +472 -0
- package/src/ui/placement/dock_styles.ts +116 -0
- package/src/ui/placement/embedded_styles.ts +14 -0
- package/src/ui/placement/launcher_drag.ts +206 -0
- package/src/ui/placement/launcher_placement.ts +133 -0
- package/src/ui/placement/launcher_styles.ts +189 -0
- package/src/ui/placement/page_styles.ts +47 -0
- package/src/ui/placement/panel_drag.ts +138 -0
- package/src/ui/placement/panel_placement.ts +1975 -0
- package/src/ui/placement/place_widget.ts +64 -0
- package/src/ui/placement/preset_styles.ts +49 -0
- package/src/ui/placement/resize_handle.ts +229 -0
- package/src/ui/placement/resize_styles.ts +201 -0
- package/src/ui/placement/sidebar_styles.ts +113 -0
- package/src/ui/placement/small_viewport_styles.ts +70 -0
- package/src/ui/placement/surface_for.ts +183 -0
- package/src/ui/progress/prettify_tool_name.ts +16 -0
- package/src/ui/progress/run_announcer.ts +67 -0
- package/src/ui/progress/run_notice.ts +61 -0
- package/src/ui/progress/run_notice_styles.ts +52 -0
- package/src/ui/progress/subagent_panel.ts +227 -0
- package/src/ui/progress/subagent_progress.ts +200 -0
- package/src/ui/progress/subagent_update.ts +89 -0
- package/src/ui/progress/thoughts_block.ts +165 -0
- package/src/ui/progress/thoughts_styles.ts +74 -0
- package/src/ui/progress/tool_call_card.ts +429 -0
- package/src/ui/progress/tool_group_styles.ts +313 -0
- package/src/ui/progress/tool_step_group.ts +317 -0
- package/src/ui/progress/tool_step_styles.ts +486 -0
- package/src/ui/shell/adopt_styles.ts +34 -0
- package/src/ui/shell/build_header.ts +121 -0
- package/src/ui/shell/build_launcher.ts +79 -0
- package/src/ui/shell/collapse_glyph.ts +42 -0
- package/src/ui/shell/frame_styles.ts +147 -0
- package/src/ui/shell/glyph_slot.ts +12 -0
- package/src/ui/shell/header_button.ts +22 -0
- package/src/ui/shell/icon_element.ts +33 -0
- package/src/ui/shell/is_unread_badge_enabled.ts +11 -0
- package/src/ui/shell/read_launcher_icon_url.ts +9 -0
- package/src/ui/shell/theme_styles.ts +11 -0
- package/src/ui/shell/token_styles.ts +480 -0
- package/src/ui/styles.ts +110 -0
- package/src/ui/themes/classic_styles.ts +32 -0
- package/src/ui/themes/classic_tokens.ts +51 -0
- package/src/ui/themes/code_tokens.ts +21 -0
- package/src/ui/themes/ledger_styles.ts +169 -0
- package/src/ui/themes/ledger_tokens.ts +33 -0
- package/src/ui/themes/modern_styles.ts +80 -0
- package/src/ui/themes/quiet_styles.ts +118 -0
- package/src/ui/themes/scheme_styles.ts +26 -0
- package/src/ui/themes/soft_styles.ts +149 -0
- package/src/ui/themes/soft_tokens.ts +29 -0
- package/src/ui/themes/theme_hosts.ts +14 -0
- package/src/ui/transcript/answer_actions.ts +165 -0
- package/src/ui/transcript/answer_stream.ts +123 -0
- package/src/ui/transcript/attachment_chip_styles.ts +116 -0
- package/src/ui/transcript/attachment_chips.ts +82 -0
- package/src/ui/transcript/build_transcript.ts +67 -0
- package/src/ui/transcript/greeting_styles.ts +171 -0
- package/src/ui/transcript/groups_tool_steps.ts +16 -0
- package/src/ui/transcript/message_action_styles.ts +155 -0
- package/src/ui/transcript/message_actions.ts +234 -0
- package/src/ui/transcript/message_styles.ts +262 -0
- package/src/ui/transcript/render_markdown.ts +181 -0
- package/src/ui/transcript/render_or_warn.ts +22 -0
- package/src/ui/transcript/reveal_words.ts +41 -0
- package/src/ui/transcript/starter_chips.ts +42 -0
- package/src/ui/transcript/stick_to_bottom.ts +126 -0
- package/src/ui/transcript/suggestion_chips.ts +73 -0
- package/src/ui/transcript/transcript.ts +555 -0
- package/src/ui/transcript/transcript_styles.ts +153 -0
- package/src/ui/ui_strings.ts +550 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,1169 @@
|
|
|
1
|
+
import { randomUUID } from "@ag-ui/client";
|
|
2
|
+
import { contentToText, type Message } from "@ag-ui/core";
|
|
3
|
+
import { MESSAGE_ROLE, TOOL_CALL_STATUS, TOOL_OUTCOME } from "../../constants.js";
|
|
4
|
+
import type { ActivityRegistry } from "../../core/activity_registry.js";
|
|
5
|
+
import type { AgUiClient } from "../../core/agui_client.js";
|
|
6
|
+
import { messageAttachments } from "../../core/attachment.js";
|
|
7
|
+
import type { ClientSeed } from "../../core/client_seed.js";
|
|
8
|
+
import type {
|
|
9
|
+
ClientConversationStore,
|
|
10
|
+
NavigationCheckpoint,
|
|
11
|
+
ThreadMeta,
|
|
12
|
+
} from "../../core/conversation_store.js";
|
|
13
|
+
import type { ThreadDetail } from "../../core/events/thread_detail.js";
|
|
14
|
+
import type { MessageRole } from "../../core/message_role.js";
|
|
15
|
+
import { RunIndex } from "../../core/run_index.js";
|
|
16
|
+
import { toolStatusFromOutcome } from "../../core/tool_outcome.js";
|
|
17
|
+
import { answerUnansweredCalls, metadataOrTopLevel, mintThread } from "../../core/utils.js";
|
|
18
|
+
import type { ToolCatalog } from "../../tools/tool_catalog.js";
|
|
19
|
+
import type { AnswerActions } from "../transcript/answer_actions.js";
|
|
20
|
+
import { renderAttachmentChips } from "../transcript/attachment_chips.js";
|
|
21
|
+
import type { Transcript } from "../transcript/transcript.js";
|
|
22
|
+
import type { UiStrings } from "../ui_strings.js";
|
|
23
|
+
import type { CheckpointMenu, CheckpointVerb } from "./checkpoint_menu.js";
|
|
24
|
+
import type { RelativeTimeFormatter } from "./relative_time.js";
|
|
25
|
+
import type { ThreadDrawer } from "./thread_drawer.js";
|
|
26
|
+
|
|
27
|
+
/** What conversation history needs from the element that owns it. */
|
|
28
|
+
export interface ConversationHistoryHost {
|
|
29
|
+
/**
|
|
30
|
+
* The custom element: the attributes history reads and stamps, and the
|
|
31
|
+
* `this` a host's `agentFactory` and `navigationResult` are called with.
|
|
32
|
+
*/
|
|
33
|
+
readonly element: HTMLElement;
|
|
34
|
+
/** The conversation list, which the element builds and renders. */
|
|
35
|
+
readonly drawer: ThreadDrawer;
|
|
36
|
+
/** The checkpoint panel, which the element builds and renders. */
|
|
37
|
+
readonly checkpoints: CheckpointMenu;
|
|
38
|
+
/** The transcript a restored conversation is replayed into. */
|
|
39
|
+
readonly transcript: Transcript;
|
|
40
|
+
/** The action row under each finished answer. */
|
|
41
|
+
readonly actions: AnswerActions;
|
|
42
|
+
/** The activity renderers, which redraw a restored activity. */
|
|
43
|
+
readonly activities: ActivityRegistry;
|
|
44
|
+
/** The frontend tools, whose renderers a restored call is redrawn with. */
|
|
45
|
+
readonly tools: ToolCatalog;
|
|
46
|
+
/** The composer, whose text a continuation sends. */
|
|
47
|
+
readonly input: HTMLTextAreaElement;
|
|
48
|
+
/** The one-line hint above the composer. */
|
|
49
|
+
readonly hint: HTMLDivElement;
|
|
50
|
+
/** The resolved string table. */
|
|
51
|
+
readonly strings: () => UiStrings;
|
|
52
|
+
/** The element's `conversationStore`, read per use. */
|
|
53
|
+
readonly conversationStore: () => ClientConversationStore;
|
|
54
|
+
/** The element's `formatRelativeTime`. */
|
|
55
|
+
readonly formatRelativeTime: () => RelativeTimeFormatter | null;
|
|
56
|
+
/** The element's `navigationResult`. */
|
|
57
|
+
readonly navigationResult: () => (checkpoint: NavigationCheckpoint) => unknown;
|
|
58
|
+
/** The request headers, having first reported the destination if it is foreign. */
|
|
59
|
+
readonly headersFor: (url: string) => Record<string, string>;
|
|
60
|
+
/** The configured cookie policy as `fetch` spells it. */
|
|
61
|
+
readonly requestCredentials: () => RequestCredentials | undefined;
|
|
62
|
+
/** The element's `appendMessage`, which a restored bubble opens through. */
|
|
63
|
+
readonly appendMessage: (role: MessageRole, content: string) => HTMLDivElement;
|
|
64
|
+
/**
|
|
65
|
+
* Draw a turn the user sent and dispatch the submit event, as the element's
|
|
66
|
+
* own send does before it starts a run.
|
|
67
|
+
*/
|
|
68
|
+
readonly announceTurn: (content: string) => void;
|
|
69
|
+
/**
|
|
70
|
+
* Record a turn taken from the composer for arrow-key recall, as the
|
|
71
|
+
* element's own send records one.
|
|
72
|
+
*/
|
|
73
|
+
readonly recordTurn: (content: string) => void;
|
|
74
|
+
/** Resize the composer to its content. */
|
|
75
|
+
readonly autoGrow: () => void;
|
|
76
|
+
/**
|
|
77
|
+
* A continuation has ended, whether or not it ever ran.
|
|
78
|
+
*
|
|
79
|
+
* The composer parks a turn typed while one is in flight, and learns a run
|
|
80
|
+
* has settled from its own events -- which never arrive for a continuation
|
|
81
|
+
* that failed before starting one. Without this, such a turn stayed parked
|
|
82
|
+
* with nothing left to release it.
|
|
83
|
+
*/
|
|
84
|
+
readonly continuationEnded: () => void;
|
|
85
|
+
/** The conversation's own client, or `null` until one is built. */
|
|
86
|
+
readonly client: () => AgUiClient | null;
|
|
87
|
+
/** The conversation's own client, built on first use. */
|
|
88
|
+
readonly ensureClient: () => AgUiClient;
|
|
89
|
+
/**
|
|
90
|
+
* A client from the same construction as the conversation's own, differing
|
|
91
|
+
* only in what `seed` says. A continuation's comes from here so that nothing
|
|
92
|
+
* added to one can be missing from the other.
|
|
93
|
+
*/
|
|
94
|
+
readonly buildClient: (seed: ClientSeed) => AgUiClient;
|
|
95
|
+
/**
|
|
96
|
+
* Forget the conversation's own client, so the next one is built from
|
|
97
|
+
* {@link ConversationHistory.restored}. Nothing is cancelled: this is for a
|
|
98
|
+
* client that is not running.
|
|
99
|
+
*/
|
|
100
|
+
readonly releaseClient: () => void;
|
|
101
|
+
/**
|
|
102
|
+
* Whether an interaction is in flight, which the composer owns: from the
|
|
103
|
+
* moment a send or a retry is taken, or a restore starts loading, not only
|
|
104
|
+
* once its run has started.
|
|
105
|
+
*/
|
|
106
|
+
readonly running: () => boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Hold the composer as a send holds it from the moment it is taken: the
|
|
109
|
+
* built-in Send parks its turn, a Retry and a pick refuse. Let go of when the
|
|
110
|
+
* run starts, by a Stop, or by {@link RunHold.release}.
|
|
111
|
+
*
|
|
112
|
+
* A host's send refuses too, unless `restored` is given: then the hold is a
|
|
113
|
+
* restore's with no run to resume, and a host's send waits for `restored` to
|
|
114
|
+
* settle and sends after it, with the conversation drawn. `restored` settles
|
|
115
|
+
* once that restore has ended, or has been stopped, and never rejects.
|
|
116
|
+
*/
|
|
117
|
+
readonly holdRun: (restored: Promise<void> | null) => RunHold;
|
|
118
|
+
/** Stop the in-flight run. */
|
|
119
|
+
readonly cancelRun: () => void;
|
|
120
|
+
/** Drop the in-memory run and transcript, leaving the thread untouched. */
|
|
121
|
+
readonly resetState: () => void;
|
|
122
|
+
/** Swap the composer between Send and Stop, which the composer owns. */
|
|
123
|
+
readonly setRunning: (running: boolean) => void;
|
|
124
|
+
/**
|
|
125
|
+
* Say the conversation on screen changed, once its transcript is replaced.
|
|
126
|
+
* The element fills in the thread it replaced, which it last reported.
|
|
127
|
+
*/
|
|
128
|
+
readonly threadChanged: (threadId: string, reason: ThreadDetail["reason"]) => void;
|
|
129
|
+
/** Hear the rows the conversation list was just given, for the title on screen. */
|
|
130
|
+
readonly threadsListed: (threads: readonly ThreadMeta[]) => void;
|
|
131
|
+
/** A thread was deleted, so whatever the element kept about it goes too. */
|
|
132
|
+
readonly threadDeleted: (threadId: string) => void;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A hold {@link ConversationHistoryHost.holdRun} took.
|
|
137
|
+
*
|
|
138
|
+
* It says nothing about whether the restore that took it was stopped. A
|
|
139
|
+
* restore asks its generation that, because a host's send that waited on a
|
|
140
|
+
* restore with no run to resume takes the hold over from it, and a restore
|
|
141
|
+
* stopped by a newer one finds that one's hold in its place -- see
|
|
142
|
+
* {@link ConversationHistory.rehydrate}.
|
|
143
|
+
*/
|
|
144
|
+
interface RunHold {
|
|
145
|
+
/**
|
|
146
|
+
* Let go, if nothing else has, and send what the composer parked behind it,
|
|
147
|
+
* because no run is going to settle and send it.
|
|
148
|
+
*/
|
|
149
|
+
readonly release: () => void;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The conversation a restore set out to restore: which restore, by its
|
|
154
|
+
* generation, and the store and thread it reads.
|
|
155
|
+
*/
|
|
156
|
+
interface RestoreClaim {
|
|
157
|
+
readonly generation: number;
|
|
158
|
+
readonly store: ClientConversationStore;
|
|
159
|
+
readonly threadId: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Which conversation is on screen, and how one gets there: restoring it from
|
|
164
|
+
* the store, switching to another from the conversation list, and continuing
|
|
165
|
+
* a run from the checkpoint panel.
|
|
166
|
+
*
|
|
167
|
+
* The one writer of the active thread's id and of the messages a restore
|
|
168
|
+
* seeded the next client with. The element reads both when it builds its
|
|
169
|
+
* client, and asks for a transition -- adopt the store's thread, mint a new
|
|
170
|
+
* one -- rather than writing either. Clearing the run, the transcript and the
|
|
171
|
+
* composer around a transition stays the element's, because it spans every
|
|
172
|
+
* part of it; history calls back for that.
|
|
173
|
+
*
|
|
174
|
+
* Owned one-to-one by an `<ag-ui-chat>`, and holding no state outside the
|
|
175
|
+
* instance.
|
|
176
|
+
*/
|
|
177
|
+
export class ConversationHistory {
|
|
178
|
+
readonly #host: ConversationHistoryHost;
|
|
179
|
+
/** The active thread's id. Empty until the element connects. */
|
|
180
|
+
#threadId = "";
|
|
181
|
+
/**
|
|
182
|
+
* The conversation as last written for the next client the element builds to
|
|
183
|
+
* start from: what the last restore replayed, or what a checkpoint
|
|
184
|
+
* continuation last saved after it. Emptied with the rest of the in-memory run.
|
|
185
|
+
*/
|
|
186
|
+
#restored: readonly Message[] = [];
|
|
187
|
+
/**
|
|
188
|
+
* Counts the conversations cleared away, so a continuation can tell whether
|
|
189
|
+
* the one it continued is still on screen when it saves.
|
|
190
|
+
*/
|
|
191
|
+
#cleared = 0;
|
|
192
|
+
/**
|
|
193
|
+
* Which restore may draw: bumped by every rehydrate, and by every clear of
|
|
194
|
+
* the conversation on screen. A restore whose generation is stale draws
|
|
195
|
+
* nothing more and resumes nothing: a newer one has started, or New chat, a
|
|
196
|
+
* thread switch or a `user-key` handover has cleared what it was restoring.
|
|
197
|
+
*/
|
|
198
|
+
#generation = 0;
|
|
199
|
+
/**
|
|
200
|
+
* The conversation the latest restore claimed, written by every restore as
|
|
201
|
+
* it starts; `null` before the first. It speaks for the restore now current
|
|
202
|
+
* only while its generation is the current one, because clearing the
|
|
203
|
+
* conversation moves that on. See `#forgetStopped` for the one reader.
|
|
204
|
+
*/
|
|
205
|
+
#claim: RestoreClaim | null = null;
|
|
206
|
+
/** Built lazily from `data-runs-url`; `null` when the host didn't opt in. */
|
|
207
|
+
#runIndex: RunIndex | null = null;
|
|
208
|
+
/**
|
|
209
|
+
* The checkpoint continuation in flight, so stopping the conversation's run
|
|
210
|
+
* reaches it. `null` when none is running.
|
|
211
|
+
*/
|
|
212
|
+
#continuation: AgUiClient | null = null;
|
|
213
|
+
/**
|
|
214
|
+
* Settles what a host's send waits on while the restore now current, one
|
|
215
|
+
* with no run to resume, loads and replays; `null` when no such restore is
|
|
216
|
+
* under way. Called when that restore ends, and as soon as it is stopped by
|
|
217
|
+
* anything that moves the generation, so a send waiting on a conversation
|
|
218
|
+
* that is no longer on screen hears so at once rather than when an
|
|
219
|
+
* abandoned load answers.
|
|
220
|
+
*
|
|
221
|
+
* Also what tells a pick refused for a load from one refused for a run.
|
|
222
|
+
*/
|
|
223
|
+
#loading: (() => void) | null = null;
|
|
224
|
+
/** The thread switch under way and the promise that settles it, or `null`. */
|
|
225
|
+
#switching: { readonly threadId: string; readonly done: Promise<void> } | null = null;
|
|
226
|
+
|
|
227
|
+
constructor(host: ConversationHistoryHost) {
|
|
228
|
+
this.#host = host;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The active thread's id. */
|
|
232
|
+
get threadId(): string {
|
|
233
|
+
return this.#threadId;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The conversation as last written, for seeding the next client. */
|
|
237
|
+
get restored(): readonly Message[] {
|
|
238
|
+
return this.#restored;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** The checkpoint continuation in flight, or `null` when none is running. */
|
|
242
|
+
get continuation(): AgUiClient | null {
|
|
243
|
+
return this.#continuation;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Point at the thread the store says is active. */
|
|
247
|
+
adoptActiveThread(): void {
|
|
248
|
+
this.#threadId = this.#host.conversationStore().threadId();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Make a freshly minted thread the active one. */
|
|
252
|
+
startThread(): void {
|
|
253
|
+
this.#threadId = mintThread(this.#host.conversationStore());
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Stop the checkpoint continuation in flight, if there is one.
|
|
258
|
+
*
|
|
259
|
+
* A continuation runs on a client of its own, which the element never held,
|
|
260
|
+
* so Stop cancelled the conversation's client and left this one streaming:
|
|
261
|
+
* the button read Stop and did nothing, and New chat, a thread switch or
|
|
262
|
+
* removing the element carried on drawing the resumed answer into whatever
|
|
263
|
+
* came next. The element calls this wherever it stops its own run.
|
|
264
|
+
*/
|
|
265
|
+
stopContinuation(): void {
|
|
266
|
+
this.#continuation?.cancel();
|
|
267
|
+
this.#continuation = null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Forget the messages the last restore seeded, with the rest of the run. */
|
|
271
|
+
forgetRestored(): void {
|
|
272
|
+
this.#restored = [];
|
|
273
|
+
// The element clears the conversation through here, so a continuation
|
|
274
|
+
// still saving afterwards learns the conversation is no longer this one.
|
|
275
|
+
this.#cleared += 1;
|
|
276
|
+
// A restore still loading or replaying must stop too, or New chat during a
|
|
277
|
+
// slow load would draw the conversation being left into the new one and hand
|
|
278
|
+
// it to the new chat's client as its history.
|
|
279
|
+
this.#generation += 1;
|
|
280
|
+
// A host's send waiting on that restore is addressed to the conversation
|
|
281
|
+
// just cleared. The element's Stop has let go of it already; this wakes it
|
|
282
|
+
// to find so, rather than leaving it waiting on a load nobody wants -- for
|
|
283
|
+
// good, if that load never answers. The drop tests in
|
|
284
|
+
// `ag_ui_chat_send_during_restore.test.ts` hang without it.
|
|
285
|
+
this.#endWait();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Settle what waits on the restore now loading, if anything does, and forget it. */
|
|
289
|
+
#endWait(): void {
|
|
290
|
+
const end = this.#loading;
|
|
291
|
+
this.#loading = null;
|
|
292
|
+
end?.();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Delete the active thread if nothing was ever sent in it. */
|
|
296
|
+
reapUnsent(): void {
|
|
297
|
+
// A thread nothing was ever sent in has nothing to come back to, and the
|
|
298
|
+
// drawer never listed it — so reap it here rather than strand one record
|
|
299
|
+
// per press of a button whose whole use is being pressed again.
|
|
300
|
+
if (this.#host.conversationStore().isUnsent?.(this.#threadId) === true) {
|
|
301
|
+
this.#host.conversationStore().clear(this.#threadId);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** The run index, built once from `data-runs-url`; `null` when unset. */
|
|
306
|
+
runs(): RunIndex | null {
|
|
307
|
+
const url = this.#host.element.getAttribute("data-runs-url");
|
|
308
|
+
if (url === null || url === "") {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
if (this.#runIndex === null) {
|
|
312
|
+
this.#runIndex = new RunIndex(
|
|
313
|
+
url,
|
|
314
|
+
() => this.#host.headersFor(url),
|
|
315
|
+
() => this.#host.requestCredentials(),
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
return this.#runIndex;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Open the conversation list, dismissing the checkpoint panel. */
|
|
322
|
+
openThreads(): void {
|
|
323
|
+
// Two overlapping surfaces, so opening one dismisses the other. Clicking away
|
|
324
|
+
// already covers the built-in buttons, but a host driving its own chrome
|
|
325
|
+
// through these methods raises no pointer event — and the drawer would then
|
|
326
|
+
// open *underneath* a popover still floating over it.
|
|
327
|
+
this.#host.checkpoints.close();
|
|
328
|
+
void this.refreshDrawer();
|
|
329
|
+
this.#host.drawer.open();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Open the checkpoint panel, dismissing the conversation list. */
|
|
333
|
+
openCheckpoints(): void {
|
|
334
|
+
// The other half of the pair — see `openThreads`.
|
|
335
|
+
this.#host.drawer.close();
|
|
336
|
+
void this.#refreshCheckpoints();
|
|
337
|
+
this.#host.checkpoints.open();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Switch the active conversation to an existing thread and replay it.
|
|
342
|
+
*
|
|
343
|
+
* A call for the thread a switch is already loading is handed that switch's
|
|
344
|
+
* own promise, so it settles when the thread is drawn rather than at once,
|
|
345
|
+
* before the replay.
|
|
346
|
+
*
|
|
347
|
+
* A store that rejects the load leaves the element on the thread it was on:
|
|
348
|
+
* the switch has already cleared the transcript, so it points back at the
|
|
349
|
+
* old thread and draws it again, saying nothing, and then rejects with the
|
|
350
|
+
* store's error. Left as it was, the element named a thread it was not
|
|
351
|
+
* showing and never reported it.
|
|
352
|
+
*/
|
|
353
|
+
switchThread(threadId: string): Promise<void> {
|
|
354
|
+
if (threadId === this.#threadId) {
|
|
355
|
+
return this.#switching?.threadId === threadId ? this.#switching.done : Promise.resolve();
|
|
356
|
+
}
|
|
357
|
+
const previous = this.#threadId;
|
|
358
|
+
const done = this.#switchTo(threadId, previous).finally(() => {
|
|
359
|
+
if (this.#switching === entry) {
|
|
360
|
+
this.#switching = null;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
const entry = { threadId, done };
|
|
364
|
+
this.#switching = entry;
|
|
365
|
+
return done;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async #switchTo(threadId: string, previous: string): Promise<void> {
|
|
369
|
+
this.#host.cancelRun();
|
|
370
|
+
this.#host.resetState();
|
|
371
|
+
this.#host.conversationStore().setActiveThread(threadId);
|
|
372
|
+
this.#threadId = threadId;
|
|
373
|
+
this.#host.setRunning(false);
|
|
374
|
+
try {
|
|
375
|
+
await this.rehydrate("switch");
|
|
376
|
+
} catch (error) {
|
|
377
|
+
// Only while this switch is still the one on screen: a newer switch, a
|
|
378
|
+
// new chat or a delete has moved the thread on, and undoing that would
|
|
379
|
+
// put the old one back over it.
|
|
380
|
+
if (this.#threadId === threadId) {
|
|
381
|
+
this.#threadId = previous;
|
|
382
|
+
this.#host.conversationStore().setActiveThread(previous);
|
|
383
|
+
// Drawn again without a word: the thread on screen is the one the last
|
|
384
|
+
// event reported, so there is nothing to tell a host. A store that
|
|
385
|
+
// cannot answer for it either leaves the screen as it is.
|
|
386
|
+
await this.rehydrate(null).catch(() => undefined);
|
|
387
|
+
}
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Rename a thread, then reload the list that shows its title. */
|
|
393
|
+
renameThread(threadId: string, title: string): void {
|
|
394
|
+
this.#host.conversationStore().renameThread(threadId, title);
|
|
395
|
+
void this.refreshDrawer();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Delete a thread; if it was the active one, fall back to a fresh chat. */
|
|
399
|
+
deleteThread(threadId: string): void {
|
|
400
|
+
const wasActive = threadId === this.#threadId;
|
|
401
|
+
if (wasActive) {
|
|
402
|
+
this.#host.cancelRun();
|
|
403
|
+
}
|
|
404
|
+
this.#host.conversationStore().clear(threadId);
|
|
405
|
+
this.#host.threadDeleted(threadId);
|
|
406
|
+
if (wasActive) {
|
|
407
|
+
this.#host.resetState();
|
|
408
|
+
this.adoptActiveThread();
|
|
409
|
+
this.#host.setRunning(false);
|
|
410
|
+
// No replay to wait for: the thread that took its place is a fresh one.
|
|
411
|
+
this.#host.threadChanged(this.#threadId, "delete");
|
|
412
|
+
}
|
|
413
|
+
void this.refreshDrawer();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Reload the drawer's thread list, marking the active thread. */
|
|
417
|
+
async refreshDrawer(): Promise<void> {
|
|
418
|
+
this.#host.drawer.setRelativeTimeFormatter(this.#host.formatRelativeTime());
|
|
419
|
+
const threads = await this.#host.conversationStore().listThreads();
|
|
420
|
+
this.#host.drawer.setThreads(threads, this.#threadId);
|
|
421
|
+
this.#host.threadsListed(threads);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Load the checkpoint panel with the runs that can actually be continued. */
|
|
425
|
+
async #refreshCheckpoints(): Promise<void> {
|
|
426
|
+
const index = this.runs();
|
|
427
|
+
// Pushed at render rather than at connect: `formatRelativeTime` is a
|
|
428
|
+
// property, so a host may set it long after the element mounted.
|
|
429
|
+
this.#host.checkpoints.setRelativeTimeFormatter(this.#host.formatRelativeTime());
|
|
430
|
+
this.#host.checkpoints.setRuns(index === null ? [] : await index.continuable());
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Continue `runId` as a **new** run, seeded server-side from its snapshot.
|
|
435
|
+
*
|
|
436
|
+
* Uses a short-lived agent pointed at the resume / fork endpoint and seeded
|
|
437
|
+
* with no history, because those endpoints supply the prior turns from the
|
|
438
|
+
* snapshot and re-sending them would duplicate. A separate agent makes that
|
|
439
|
+
* structural and mints the fresh `run_id` the endpoints also require.
|
|
440
|
+
*
|
|
441
|
+
* Built by the same construction as the conversation's own client, so the
|
|
442
|
+
* continuation streams into the same transcript the user is looking at and
|
|
443
|
+
* runs under the same state, tools and bounds. It is the run in flight while
|
|
444
|
+
* it lasts: {@link stopContinuation} is how the element's Stop reaches it.
|
|
445
|
+
*
|
|
446
|
+
* What it adds joins the conversation. Its saves write the conversation on
|
|
447
|
+
* screen ahead of its exchange, and each one hands that whole list to the
|
|
448
|
+
* next client the element builds, so the next ordinary message is sent with
|
|
449
|
+
* the exchange it follows. The conversation's own client never held it, and
|
|
450
|
+
* was sending without it.
|
|
451
|
+
*/
|
|
452
|
+
async continueRun(runId: string, verb: CheckpointVerb): Promise<void> {
|
|
453
|
+
const index = this.runs();
|
|
454
|
+
if (index === null) {
|
|
455
|
+
// Unreachable from the built-in control: the header button is only
|
|
456
|
+
// rendered when `runs()` is configured, so a row to pick cannot exist
|
|
457
|
+
// without one. A host calling `openCheckpoints()` regardless gets the
|
|
458
|
+
// documented empty panel, which has no rows either. Typed, not silent.
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (this.#host.running() || this.#continuation !== null) {
|
|
462
|
+
// Refused rather than started beside it. The panel opens and its rows
|
|
463
|
+
// take a pick while a run streams, and a second run would draw its answer
|
|
464
|
+
// into the turn still arriving; a second continuation would also replace
|
|
465
|
+
// the one Stop reaches, leaving the first streaming where nothing could
|
|
466
|
+
// end it. Nor is the earlier run cancelled for it: a pick in a panel is not
|
|
467
|
+
// a Stop, and what is streaming may be the answer the user is waiting on.
|
|
468
|
+
//
|
|
469
|
+
// Both checks, because they see different things. `running` is the
|
|
470
|
+
// composer's own state: it spans every round of an interaction, from the
|
|
471
|
+
// moment a send or a retry is taken rather than from its run's first
|
|
472
|
+
// event, which is a microtask behind it. A continuation is not the
|
|
473
|
+
// composer's, and is recorded here the moment it starts.
|
|
474
|
+
//
|
|
475
|
+
// Said at the composer, as an empty composer is below, because the row
|
|
476
|
+
// closed the panel before this ran. The typed turn stays where it is --
|
|
477
|
+
// it is what the user wants sent once the run is done -- and the caret
|
|
478
|
+
// goes back to it, where Escape stops the run.
|
|
479
|
+
//
|
|
480
|
+
// A restore with no run to resume holds the composer too, and there is
|
|
481
|
+
// no answer to wait for and nothing Escape can stop, so it says the
|
|
482
|
+
// conversation is still loading instead. A restore that will resume a
|
|
483
|
+
// run keeps the running wording: what it is loading is that run.
|
|
484
|
+
const strings = this.#host.strings();
|
|
485
|
+
this.#refuse(
|
|
486
|
+
this.#loading === null ? strings.continueWhileRunning : strings.continueWhileLoading,
|
|
487
|
+
);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const content = this.#host.input.value.trim();
|
|
491
|
+
if (content === "") {
|
|
492
|
+
// A continuation sends *only* the next turn -- the snapshot supplies
|
|
493
|
+
// everything before it -- so with an empty composer there is nothing to
|
|
494
|
+
// send. Returning here was the same failure the endpoint guard above had:
|
|
495
|
+
// the row's button closes the panel before this runs, so the widget
|
|
496
|
+
// visibly reacted and then did nothing, which reads as a resume that was
|
|
497
|
+
// attempted and lost rather than one that never started.
|
|
498
|
+
//
|
|
499
|
+
// Said at the composer rather than in the transcript, because that is
|
|
500
|
+
// where the fix goes and because the hint clears itself on the first
|
|
501
|
+
// keystroke -- a transcript notice for a recoverable slip would outlive
|
|
502
|
+
// the slip. Focus follows for the same reason applying a skill moves it when
|
|
503
|
+
// a template is short of a field.
|
|
504
|
+
this.#refuse(this.#host.strings().continueNeedsTurn);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
// Typed in the composer and sent by a press, so it is recallable as any
|
|
508
|
+
// other turn is. Recorded as it leaves the box, where the element's own
|
|
509
|
+
// send records one, rather than once it is sent: the box is empty from
|
|
510
|
+
// here, and a continuation that fails before its run starts has still
|
|
511
|
+
// taken the turn out of it.
|
|
512
|
+
this.#host.recordTurn(content);
|
|
513
|
+
this.#host.input.value = "";
|
|
514
|
+
this.#host.autoGrow();
|
|
515
|
+
const cleared = this.#cleared;
|
|
516
|
+
const client = this.#host.buildClient({
|
|
517
|
+
endpoint: verb === "resume" ? index.resumeUrl(runId) : index.forkUrl(runId),
|
|
518
|
+
// The seed the endpoints assume: nothing. The snapshot is the history.
|
|
519
|
+
initialMessages: [],
|
|
520
|
+
// What its saves write ahead of the exchange: the conversation on screen,
|
|
521
|
+
// in the form the store holds it. From the conversation's own client when
|
|
522
|
+
// there is one, because that client keeps how each call ended beside its
|
|
523
|
+
// messages rather than on them; otherwise what the last restore or
|
|
524
|
+
// continuation wrote, which is already in that form.
|
|
525
|
+
//
|
|
526
|
+
// All of it, even where this forks an earlier run and the server's
|
|
527
|
+
// snapshot stops there: what is saved is what the screen shows.
|
|
528
|
+
follows: this.#host.client()?.messages ?? this.#restored,
|
|
529
|
+
onSaved: (conversation) => {
|
|
530
|
+
// A continuation stopped by New chat or a thread switch saves once its
|
|
531
|
+
// request closes, into its own thread; the conversation now on screen
|
|
532
|
+
// is not the one it continued.
|
|
533
|
+
if (cleared !== this.#cleared) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
// The conversation's own client holds the conversation without this
|
|
537
|
+
// exchange, and would send it that way. Released rather than patched:
|
|
538
|
+
// the next client is built from this list exactly as a reload builds
|
|
539
|
+
// one from the store, outcomes included.
|
|
540
|
+
//
|
|
541
|
+
// Nothing can run on the released client in between, because the
|
|
542
|
+
// composer refuses to start one while a continuation is in flight --
|
|
543
|
+
// it parks the turn instead. It said here that only a script in the
|
|
544
|
+
// same task could, which was wrong by a whole request: `running` does
|
|
545
|
+
// not turn on until the continuation's first event, and a person who
|
|
546
|
+
// typed in that window got a second run against the snapshot
|
|
547
|
+
// `follows` had already frozen, so whichever saved last dropped the
|
|
548
|
+
// other's turn.
|
|
549
|
+
this.#restored = conversation;
|
|
550
|
+
this.#host.releaseClient();
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
this.#continuation = client;
|
|
554
|
+
try {
|
|
555
|
+
// Drawn and announced as the element's own send does, because this is
|
|
556
|
+
// the same act sent somewhere else. Without the bubble the answer arrived
|
|
557
|
+
// under no question, while the save below wrote the turn, so a reload
|
|
558
|
+
// showed what the live transcript never had.
|
|
559
|
+
//
|
|
560
|
+
// The submit event fires too. A host listens to it as "the user sent
|
|
561
|
+
// something", which a continued turn is: typed in the composer and sent
|
|
562
|
+
// by a press. Only the endpoint differs, and the host chose that when it
|
|
563
|
+
// configured the panel; leaving it out would make a host's own record of
|
|
564
|
+
// the conversation miss exactly the turns this panel exists to add.
|
|
565
|
+
//
|
|
566
|
+
// Only once the continuation is recorded, because a listener runs here:
|
|
567
|
+
// a host's own send from it is refused as overlapping the run in flight,
|
|
568
|
+
// as it would be from any later moment of this one.
|
|
569
|
+
this.#host.announceTurn(content);
|
|
570
|
+
// A listener can also have stopped it: New chat, a thread switch, or
|
|
571
|
+
// moving the element all reach the stop. That cancelled a client whose
|
|
572
|
+
// run had not begun, and a run resets its own cancellation when it
|
|
573
|
+
// starts, so sending now would start one that nothing holds and nothing
|
|
574
|
+
// can stop -- in a conversation the user has already left, with its
|
|
575
|
+
// tools still driving the page.
|
|
576
|
+
if (this.#continuation !== client) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
await client.send(content);
|
|
580
|
+
} finally {
|
|
581
|
+
// Only if it is still the one in flight: stopping forgets it at once, and
|
|
582
|
+
// a continuation started after that one is not this one to forget.
|
|
583
|
+
//
|
|
584
|
+
// In a finally because the send can fail before its run ever starts. The
|
|
585
|
+
// first save goes through the host's `conversationStore` synchronously
|
|
586
|
+
// inside it, and a store is the host's to replace: the built-in one
|
|
587
|
+
// swallows a write the browser refused, a server-backed one need not. A
|
|
588
|
+
// throw there left this pointing at a client that would never run, so
|
|
589
|
+
// every later pick was refused with no Stop to clear it -- the composer's
|
|
590
|
+
// button is Send until a run reports a start -- and `#liveClient` went on
|
|
591
|
+
// handing that dead client the shared state a host wrote.
|
|
592
|
+
if (this.#continuation === client) {
|
|
593
|
+
this.#continuation = null;
|
|
594
|
+
this.#host.continuationEnded();
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Say at the composer why a picked run did not continue, and put the caret
|
|
601
|
+
* there. The hint clears itself on the next keystroke.
|
|
602
|
+
*/
|
|
603
|
+
#refuse(reason: string): void {
|
|
604
|
+
this.#host.hint.textContent = reason;
|
|
605
|
+
this.#host.hint.hidden = false;
|
|
606
|
+
this.#host.input.focus();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Restore the conversation from the store on mount, then — if a navigating
|
|
611
|
+
* tool reloaded the page mid-run — resume the loop by supplying that tool's
|
|
612
|
+
* result from the page we landed on.
|
|
613
|
+
*
|
|
614
|
+
* Any other call the stored run left unanswered is answered on the way in, as
|
|
615
|
+
* not finished, because no run is left to answer it.
|
|
616
|
+
*
|
|
617
|
+
* **What the store holds after a reload mid-run.** The run loop persists a
|
|
618
|
+
* round when its stream ends, which is *before* it asks about a gated call,
|
|
619
|
+
* runs a frontend tool, or collects a server-side approval. A reload in that
|
|
620
|
+
* window leaves the round's calls stored with no result, and the request that
|
|
621
|
+
* would have produced one died with the page.
|
|
622
|
+
*
|
|
623
|
+
* **Why not finished, and not declined.** Stop declines an open card because
|
|
624
|
+
* pressing it is a person answering the question. A reload answers nothing, and
|
|
625
|
+
* the stored shape is the same whether the round waited on a person or on a
|
|
626
|
+
* handler the reload killed, so declined would be unproven for the first and
|
|
627
|
+
* false for the second. A marker saved while a card is open could tell them
|
|
628
|
+
* apart, but every host store would have to round-trip it, and a store that
|
|
629
|
+
* dropped it would fall back to this wording anyway -- which is true of both.
|
|
630
|
+
*
|
|
631
|
+
* The answers come from the same helper the client runs before each request,
|
|
632
|
+
* so a restored card and the result the next request carries cannot disagree.
|
|
633
|
+
* Giving them to the replay, rather than leaving the client to add them, is
|
|
634
|
+
* what settles each card from its result. The checkpointed call is excluded,
|
|
635
|
+
* because the resume path answers it from the page the reload landed on; that
|
|
636
|
+
* exclusion is held by "resumes with the landed page's result, and its card says
|
|
637
|
+
* so" in `ag_ui_chat_reload_mid_run.test.ts`.
|
|
638
|
+
*
|
|
639
|
+
* **Every restore holds the composer from before the load.** Until the
|
|
640
|
+
* store answers, the conversation's client can only be seeded from nothing.
|
|
641
|
+
* With a remote store that is a real request, and a send, a Retry or a pick
|
|
642
|
+
* in it was refused by nothing: each built the conversation's client before
|
|
643
|
+
* the conversation had loaded, so a send went out carrying none of it, its
|
|
644
|
+
* first save replaced the stored one, and the replay then drew the stored
|
|
645
|
+
* conversation under the new turn. The client stayed cached without it, so
|
|
646
|
+
* every later turn went out without it too. So the composer is held as a
|
|
647
|
+
* send holds it: what was typed parks and goes out after the replay, and a
|
|
648
|
+
* Retry and a pick are refused -- a Retry has nothing on screen to retry, and
|
|
649
|
+
* after the load it would re-ask the previous visit's last question.
|
|
650
|
+
*
|
|
651
|
+
* **A host's send waits for a restore with no run to resume.** It is
|
|
652
|
+
* usually a prompt fired as the element mounts, which no host could time
|
|
653
|
+
* against a load nothing announces, and refusing it would lose the turn.
|
|
654
|
+
* So it waits for this restore to end, taking the hold over as it does,
|
|
655
|
+
* and then sends with the conversation drawn above it; what the built-in
|
|
656
|
+
* Send parked goes out after its run. Stopped while it waits, it is dropped,
|
|
657
|
+
* as a send stopped by its own submit listener is.
|
|
658
|
+
*
|
|
659
|
+
* **A restore that resumes refuses a host's send instead.** The resume is a
|
|
660
|
+
* run, and the restore ends by starting it, so a send that waited would only
|
|
661
|
+
* reach that run and be refused there. The resume gives way to nothing: what
|
|
662
|
+
* was typed parks behind the resumed run and goes out after its answer.
|
|
663
|
+
*
|
|
664
|
+
* The other way round, the resume standing down for the send, was rejected.
|
|
665
|
+
* The send that got in first had already gone out without the conversation,
|
|
666
|
+
* so standing down kept that loss, and it also dropped the run the navigation
|
|
667
|
+
* was part of, which the user asked for before the reload: the agent stopped
|
|
668
|
+
* halfway through the task, with the navigating call never answered.
|
|
669
|
+
*
|
|
670
|
+
* **A restore stops where it was stopped, held or not.** New chat, a thread
|
|
671
|
+
* switch, a `user-key` handover, a newer restore or the element leaving the
|
|
672
|
+
* page can each come while the store answers, and from host code the restore
|
|
673
|
+
* runs itself: the activity and tool renderers each replayed message draws
|
|
674
|
+
* through, and `navigationResult`. From then on it draws nothing more and
|
|
675
|
+
* resumes nothing. The hold cannot say so, because a host's send that
|
|
676
|
+
* waited may hold it by then. So the restore asks its generation, which clearing
|
|
677
|
+
* the conversation moves on as a newer restore does, and asks it after the
|
|
678
|
+
* load, after each replayed message -- as a Retry asks after each one -- and
|
|
679
|
+
* once more before the resume. Stopped by anything but the element leaving
|
|
680
|
+
* the page, it also forgets the checkpoint it loaded, since that was a Stop
|
|
681
|
+
* for the run it would have resumed -- unless what replaced it is a restore
|
|
682
|
+
* of the same conversation, still current, which answers for that run
|
|
683
|
+
* instead; see `#forgetStopped`.
|
|
684
|
+
*
|
|
685
|
+
* `reason` is what the thread event this restore ends with reports as why
|
|
686
|
+
* the conversation changed. A restore that is stopped reports nothing, and
|
|
687
|
+
* neither does one given `null`, which draws the thread already reported.
|
|
688
|
+
*/
|
|
689
|
+
async rehydrate(reason: ThreadDetail["reason"] | null): Promise<void> {
|
|
690
|
+
// Claimed before awaiting, and checked by `#restoring` wherever the restore
|
|
691
|
+
// has waited or run host code. With a slow remote store, picking thread B
|
|
692
|
+
// then C interleaved both replays into one transcript, and New chat drew
|
|
693
|
+
// the conversation being left into the new one.
|
|
694
|
+
this.#generation += 1;
|
|
695
|
+
const generation = this.#generation;
|
|
696
|
+
// Read before the load rather than after it, because a checkpoint means
|
|
697
|
+
// this restore ends by starting a run, and the window a send could start a
|
|
698
|
+
// second one in is the load, not the resume -- see "A restore that resumes"
|
|
699
|
+
// above. The store's checkpoint is synchronous and the built-in remote store
|
|
700
|
+
// keeps it in this tab, so nothing the load fetches can change it. And
|
|
701
|
+
// before the replay, which has to know the one unanswered call a reload was
|
|
702
|
+
// *expected* by, so as not to settle it as abandoned.
|
|
703
|
+
//
|
|
704
|
+
// The thread and the store are taken once, here, because a restore that is
|
|
705
|
+
// stopped forgets its checkpoint where it read it: by then the active
|
|
706
|
+
// thread can be a new chat's, and the store scoped to the next principal.
|
|
707
|
+
const threadId = this.#threadId;
|
|
708
|
+
const store = this.#host.conversationStore();
|
|
709
|
+
// Claimed with the generation, store and thread just taken, whether or not
|
|
710
|
+
// there is a checkpoint: a restore of the same conversation that finds none
|
|
711
|
+
// has found it resumed or forgotten already, so an older one has nothing
|
|
712
|
+
// left there to forget. Claiming only with a checkpoint would differ only
|
|
713
|
+
// where one was written after this restore started, which an older restore
|
|
714
|
+
// should not wipe either, so no test tells the two apart. See
|
|
715
|
+
// `#claimedByCurrent`.
|
|
716
|
+
this.#claim = { generation, store, threadId };
|
|
717
|
+
const checkpoint = store.loadCheckpoint(threadId);
|
|
718
|
+
// What a host's send waits on, for a restore with no run to resume, and
|
|
719
|
+
// the hold either kind takes. Taken before the load for the same reason
|
|
720
|
+
// the checkpoint is read before it: the load is the window.
|
|
721
|
+
//
|
|
722
|
+
// No earlier wait is left to settle here: every restore but the first
|
|
723
|
+
// follows a clear of the conversation on screen -- a reload, a thread
|
|
724
|
+
// switch, a handover, the element coming back -- and `forgetRestored`
|
|
725
|
+
// settled and forgot it then. So a restore that will resume finds none.
|
|
726
|
+
const restored =
|
|
727
|
+
checkpoint === null
|
|
728
|
+
? new Promise<void>((resolve) => {
|
|
729
|
+
this.#loading = resolve;
|
|
730
|
+
})
|
|
731
|
+
: null;
|
|
732
|
+
const waiting = this.#loading;
|
|
733
|
+
const hold = this.#host.holdRun(restored);
|
|
734
|
+
try {
|
|
735
|
+
// Held while the store answers. A remote store answers after first paint,
|
|
736
|
+
// and a conversation it is still fetching is more likely to have messages
|
|
737
|
+
// than not, so without this the page would paint the greeting and a centred
|
|
738
|
+
// composer and then drop the composer the moment they land. The built-in
|
|
739
|
+
// store answers in a microtask, before paint, so for it this never reaches
|
|
740
|
+
// the screen. Released in `finally` so a store that rejects cannot leave the
|
|
741
|
+
// layout held for good, and only by the restore that is still current.
|
|
742
|
+
this.#host.element.setAttribute("data-restoring", "");
|
|
743
|
+
let messages: readonly Message[] | null;
|
|
744
|
+
try {
|
|
745
|
+
messages = await store.loadMessages(threadId);
|
|
746
|
+
} finally {
|
|
747
|
+
if (generation === this.#generation) {
|
|
748
|
+
this.#host.element.removeAttribute("data-restoring");
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
// Stopped while the store answered. Going on drew the conversation being
|
|
752
|
+
// left into the chat that replaced it, and resumed its run there, on a
|
|
753
|
+
// client seeded from what the restore had just drawn.
|
|
754
|
+
if (!this.#restoring(generation)) {
|
|
755
|
+
this.#forgetStopped(store, threadId, checkpoint);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (messages !== null) {
|
|
759
|
+
const unfinished = this.#host.strings().callNotFinished;
|
|
760
|
+
const restored = answerUnansweredCalls(
|
|
761
|
+
messages,
|
|
762
|
+
// The shape the store holds for a call the client answered the same
|
|
763
|
+
// way: its tool message, with the outcome in its metadata.
|
|
764
|
+
(toolCallId) => ({
|
|
765
|
+
id: randomUUID(),
|
|
766
|
+
role: "tool",
|
|
767
|
+
content: unfinished,
|
|
768
|
+
toolCallId,
|
|
769
|
+
metadata: { outcome: TOOL_OUTCOME.INTERRUPTED },
|
|
770
|
+
}),
|
|
771
|
+
new Set(checkpoint === null ? [] : [checkpoint.toolCallId]),
|
|
772
|
+
);
|
|
773
|
+
this.#restored = restored;
|
|
774
|
+
for (const message of restored) {
|
|
775
|
+
this.replay(message);
|
|
776
|
+
// A replay draws through the host's activity and tool renderers,
|
|
777
|
+
// which can stop the restore as well as anything outside it can.
|
|
778
|
+
// Checked after each one, as a Retry checks, so the rest of the
|
|
779
|
+
// conversation being left is not drawn into the chat that replaced it,
|
|
780
|
+
// and after the last, so its run is not resumed there.
|
|
781
|
+
if (!this.#restoring(generation)) {
|
|
782
|
+
this.#forgetStopped(store, threadId, checkpoint);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (checkpoint === null) {
|
|
788
|
+
this.#noticeIfRunUnfinished(messages);
|
|
789
|
+
}
|
|
790
|
+
// Said once the transcript is the restored one, and only by a restore
|
|
791
|
+
// still current: one that was stopped returned above, so two quick
|
|
792
|
+
// switches report the second alone. Before a resume, because the resume
|
|
793
|
+
// is a run, and waiting for it reported the change a whole answer late.
|
|
794
|
+
if (reason !== null) {
|
|
795
|
+
this.#host.threadChanged(threadId, reason);
|
|
796
|
+
}
|
|
797
|
+
if (checkpoint !== null) {
|
|
798
|
+
// A listener is host code, and can stop the restore as a replayed
|
|
799
|
+
// renderer can. Resuming then answered the call into the conversation
|
|
800
|
+
// that replaced it, and left the checkpoint for a later visit to resume.
|
|
801
|
+
if (!this.#restoring(generation)) {
|
|
802
|
+
this.#forgetStopped(store, threadId, checkpoint);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
await this.#resumeFrom(checkpoint, generation);
|
|
806
|
+
}
|
|
807
|
+
} finally {
|
|
808
|
+
// The resumed run starting has let go of the hold already, as a send's
|
|
809
|
+
// does, and a request that then fails still started one: the client
|
|
810
|
+
// reports a run before it sends. Still held here only if none started --
|
|
811
|
+
// the store rejected the load, or the store or the host threw on the way
|
|
812
|
+
// to the resume -- and then this is what sends a turn the composer parked
|
|
813
|
+
// meanwhile, since no run is going to settle and send it.
|
|
814
|
+
//
|
|
815
|
+
// A host's send that waited on this restore has taken the hold over, so
|
|
816
|
+
// the release does nothing and the send goes first; what was parked goes
|
|
817
|
+
// out after its run. Woken only after the replay, here, rather than when
|
|
818
|
+
// the store answers, or its turn would be drawn above the conversation.
|
|
819
|
+
// Only this restore's wait: one that was stopped was woken by what
|
|
820
|
+
// stopped it, and the wait now current, if any, is a newer restore's.
|
|
821
|
+
// Waking that one here sent its turn before its conversation was drawn,
|
|
822
|
+
// which "drops the waiting send on a drawer switch, and a send then
|
|
823
|
+
// carries the picked thread" in `ag_ui_chat_send_during_restore.test.ts`
|
|
824
|
+
// holds.
|
|
825
|
+
hold.release();
|
|
826
|
+
if (this.#loading === waiting) {
|
|
827
|
+
this.#endWait();
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Whether the restore that claimed `generation` may go on: nothing has
|
|
834
|
+
* replaced or cleared the conversation it is restoring, and the element is
|
|
835
|
+
* still on the page.
|
|
836
|
+
*
|
|
837
|
+
* On the page, because leaving it is the one stop that clears nothing: the
|
|
838
|
+
* element is not reset until it connects again, and connecting restores the
|
|
839
|
+
* conversation afresh. A restore with nothing to resume went on regardless,
|
|
840
|
+
* drawing into a node that had gone and running the host's renderers for it.
|
|
841
|
+
* One that resumes learnt it from the Stop letting go of its hold; nothing
|
|
842
|
+
* asks the hold that now, so this is what keeps the resume's request in.
|
|
843
|
+
*
|
|
844
|
+
* One `&&`, so coverage cannot tell whether either half is held. Removing the
|
|
845
|
+
* generation fails "draws nothing into a new chat started while it loads" in
|
|
846
|
+
* `conversation_history.test.ts`, and "drops a stale thread replay when a
|
|
847
|
+
* newer switch started" in `ag_ui_chat.test.ts`; removing the connection
|
|
848
|
+
* fails "draws nothing into an element that left the page while it loads".
|
|
849
|
+
*/
|
|
850
|
+
#restoring(generation: number): boolean {
|
|
851
|
+
return generation === this.#generation && this.#host.element.isConnected;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Forget the checkpoint a stopped restore loaded, when what stopped it also
|
|
856
|
+
* stopped the run it was about to resume.
|
|
857
|
+
*
|
|
858
|
+
* New chat, a thread switch, a `user-key` handover and a newer restore all
|
|
859
|
+
* move the generation, and each is a Stop for that run. Left in the store,
|
|
860
|
+
* the checkpoint outlived the Stop: coming back to the conversation later
|
|
861
|
+
* resumed the navigating call with whatever page was current by then.
|
|
862
|
+
* Forgotten, the call is answered as not finished there, as any stopped call
|
|
863
|
+
* is.
|
|
864
|
+
*
|
|
865
|
+
* Not while the restore now current claimed the same conversation, which it
|
|
866
|
+
* has in two cases. It is this restore, when only the element left the page,
|
|
867
|
+
* which moves no generation: a move or a framework re-render is not a
|
|
868
|
+
* farewell, and the restore connecting starts has to find the checkpoint to
|
|
869
|
+
* resume from. Or it is a newer restore of the same thread in the same store
|
|
870
|
+
* -- `reload()`, switching away and back, or the element connecting again --
|
|
871
|
+
* which read the checkpoint as it started, or found it gone, and answers for
|
|
872
|
+
* it from then on: its resume clears it, and when it is stopped in turn this
|
|
873
|
+
* same rule decides.
|
|
874
|
+
*
|
|
875
|
+
* A newer restore's case does not forget it: if the element leaves the page
|
|
876
|
+
* while the newer one loads, that one stands down keeping the checkpoint for
|
|
877
|
+
* the reconnect, and a store that no longer held it would leave no run to resume.
|
|
878
|
+
*
|
|
879
|
+
* Written to the store and the thread it was read from. After a handover
|
|
880
|
+
* that is the previous principal's: the built-in store's clear is a removal
|
|
881
|
+
* from a namespace the handover has already purged, so it changes nothing,
|
|
882
|
+
* and nothing reaches the next principal's. A host's store is the same object
|
|
883
|
+
* either side of a handover; one the host replaced is held by "still refuses
|
|
884
|
+
* a send for the newer restore once the older has stood down".
|
|
885
|
+
*
|
|
886
|
+
* Two conditions. Clearing with no checkpoint fails "draws nothing into a new
|
|
887
|
+
* chat started while it loads", which runs no store code for one. Clearing
|
|
888
|
+
* what the current restore claimed fails "resumes nothing once the element
|
|
889
|
+
* leaves the page while the store answers", where the claim is this
|
|
890
|
+
* restore's own and the re-inserted element resumes, and "resumes once the
|
|
891
|
+
* element is back when the host reloads and it then leaves the page", where
|
|
892
|
+
* it is the reload's. The claim's own three are named on `#claimedByCurrent`.
|
|
893
|
+
*/
|
|
894
|
+
#forgetStopped(
|
|
895
|
+
store: ClientConversationStore,
|
|
896
|
+
threadId: string,
|
|
897
|
+
checkpoint: NavigationCheckpoint | null,
|
|
898
|
+
): void {
|
|
899
|
+
if (checkpoint !== null && !this.#claimedByCurrent(store, threadId)) {
|
|
900
|
+
store.saveCheckpoint(threadId, null);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Whether the restore now current claimed the conversation a stopped restore
|
|
906
|
+
* read its checkpoint from: the same thread, in the same store.
|
|
907
|
+
*
|
|
908
|
+
* A claim from an earlier generation speaks for nobody: the conversation was
|
|
909
|
+
* cleared after it was made, so whatever it would have resumed was stopped,
|
|
910
|
+
* and that holds of a stopped restore's own claim as much as of any other.
|
|
911
|
+
* The store is compared as an object, because a store the host assigned
|
|
912
|
+
* since is a different record of the conversation, and the one the
|
|
913
|
+
* checkpoint was read from still has to forget it.
|
|
914
|
+
*
|
|
915
|
+
* As an object, a store the element builds for itself -- the built-in one
|
|
916
|
+
* under a namespace, and the remote one `data-threads-url` wraps around it
|
|
917
|
+
* -- is a different one after a reconnect, which builds it afresh over the
|
|
918
|
+
* same storage, so a restore from before the reconnect forgets the
|
|
919
|
+
* checkpoint there once it answers. The reconnect's own restore read it as
|
|
920
|
+
* it started and resumes from that copy, as a newer restore always did here;
|
|
921
|
+
* what that copy cannot survive is the element leaving again before the
|
|
922
|
+
* load answers.
|
|
923
|
+
*
|
|
924
|
+
* One `&&` of three, so coverage cannot tell whether any is held. Dropping
|
|
925
|
+
* the generation fails "forgets the checkpoint when New chat stops the
|
|
926
|
+
* resume, so the call ends as not finished", where the claim is the stopped
|
|
927
|
+
* restore's own; dropping the store fails "still refuses a send for the newer
|
|
928
|
+
* restore once the older has stood down", whose host replaces the store
|
|
929
|
+
* between the two; dropping the thread fails "forgets the checkpoint of a
|
|
930
|
+
* thread switched away from, though the next has one of its own".
|
|
931
|
+
*/
|
|
932
|
+
#claimedByCurrent(store: ClientConversationStore, threadId: string): boolean {
|
|
933
|
+
const claim = this.#claim;
|
|
934
|
+
return (
|
|
935
|
+
claim?.generation === this.#generation && claim.store === store && claim.threadId === threadId
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Notice a previous run that never produced a response.
|
|
941
|
+
*
|
|
942
|
+
* {@link AgUiClient.send} persists the user's message before starting the
|
|
943
|
+
* run, so a transcript ending on that user message means nothing came back.
|
|
944
|
+
* The transcript's shape alone detects it, needing no store method and no
|
|
945
|
+
* `pagehide` listener — neither of which fires on a crash or force-quit.
|
|
946
|
+
*
|
|
947
|
+
* An agent-initiated reload is not this case: a navigating tool leaves a
|
|
948
|
+
* checkpoint and resumes, so the caller returns early on one.
|
|
949
|
+
*
|
|
950
|
+
* Deliberately a notice, never a resume. AG-UI has no resume-an-aborted-run
|
|
951
|
+
* primitive, and re-sending the accumulated messages is semantically a new
|
|
952
|
+
* run, so any server-side tool already executed would run a second time.
|
|
953
|
+
*/
|
|
954
|
+
#noticeIfRunUnfinished(messages: readonly Message[] | null): void {
|
|
955
|
+
const last = messages?.at(-1);
|
|
956
|
+
if (last === undefined || last.role !== MESSAGE_ROLE.USER) {
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
this.#host.transcript.appendNotice("⚠", this.#host.strings().runInterrupted, "interrupted");
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Replay a restored message: text bubbles *and* tool activity. An assistant
|
|
964
|
+
* turn may carry `toolCalls` (rendered as cards) and/or text; a `tool` turn
|
|
965
|
+
* carries a result that settles the matching card. So a refreshed page shows
|
|
966
|
+
* the full transcript — tool calls and their results — not just the prose.
|
|
967
|
+
*/
|
|
968
|
+
replay(message: Message): void {
|
|
969
|
+
const text = typeof message.content === "string" ? message.content : "";
|
|
970
|
+
if (message.role === MESSAGE_ROLE.USER) {
|
|
971
|
+
const attachments = messageAttachments(message);
|
|
972
|
+
if (text !== "" || attachments.length > 0) {
|
|
973
|
+
const bubble = this.#host.appendMessage(MESSAGE_ROLE.USER, text);
|
|
974
|
+
if (attachments.length > 0) {
|
|
975
|
+
bubble.appendChild(renderAttachmentChips(attachments));
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
if (message.role === MESSAGE_ROLE.ASSISTANT) {
|
|
981
|
+
if (text !== "") {
|
|
982
|
+
// Restored history must appear statically — entrance animations
|
|
983
|
+
// (fade / word) are for freshly-arriving messages. On reload the whole
|
|
984
|
+
// transcript mounts at once, so animating every bubble's text in
|
|
985
|
+
// parallel looks wrong. Mark it so the fade CSS skips it, and don't
|
|
986
|
+
// wrap words.
|
|
987
|
+
const restoredBubble = this.#host.appendMessage(MESSAGE_ROLE.ASSISTANT, text);
|
|
988
|
+
restoredBubble.classList.add("message--restored");
|
|
989
|
+
// The stored id, which is the streamed one wherever the store kept
|
|
990
|
+
// it. A Retry re-renders the turns it keeps through here too, so a
|
|
991
|
+
// kept answer stays the message it was. No run id: a stored message
|
|
992
|
+
// does not carry one.
|
|
993
|
+
//
|
|
994
|
+
// Narrowed rather than trusted, like the fields below: a host store or
|
|
995
|
+
// a `data-threads-url` server can return a message with no id, and a
|
|
996
|
+
// rating that names nothing is a pair of buttons that lead nowhere.
|
|
997
|
+
// Such an answer keeps its copy and loses only the rating.
|
|
998
|
+
const id: unknown = message.id;
|
|
999
|
+
this.#host.actions.attach(
|
|
1000
|
+
restoredBubble,
|
|
1001
|
+
typeof id === "string" && id !== "" ? { messageId: id } : { rateable: false },
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
// Narrowed rather than trusted, for the same reason `messageAttachments`
|
|
1005
|
+
// narrows the neighbouring field: anything that throws in this loop aborts
|
|
1006
|
+
// the replay at this message, and every later turn silently disappears from
|
|
1007
|
+
// the transcript. See `restoredToolCalls`.
|
|
1008
|
+
for (const call of restoredToolCalls(message.toolCalls)) {
|
|
1009
|
+
const restored = {
|
|
1010
|
+
id: call.id,
|
|
1011
|
+
name: call.function.name,
|
|
1012
|
+
args: this.#parseArgs(call.function.arguments),
|
|
1013
|
+
};
|
|
1014
|
+
// Restored history goes through the same interception as the live
|
|
1015
|
+
// stream — otherwise a reload resurrects the raw `load_capability`
|
|
1016
|
+
// card the live path deliberately replaced.
|
|
1017
|
+
if (this.#host.transcript.noticeIfSkillLoad(restored)) {
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
this.#host.transcript.setCardElement(
|
|
1021
|
+
restored.id,
|
|
1022
|
+
this.#host.transcript.cardFor(restored).element,
|
|
1023
|
+
);
|
|
1024
|
+
// Only `render` is replayed, never `handler`. A restored transcript
|
|
1025
|
+
// redraws what the call drew; it must not re-run what the call *did*.
|
|
1026
|
+
// Only the renderer is handed over, never the tool. The guarantee that
|
|
1027
|
+
// a reload cannot re-run a tool's *effect* is worth more than a comment
|
|
1028
|
+
// saying so: this signature cannot reach `handler`, so a later
|
|
1029
|
+
// maintainer adding a "no render? fall back to the handler" convenience
|
|
1030
|
+
// here has to change the type first, which is exactly the moment the
|
|
1031
|
+
// question should be asked.
|
|
1032
|
+
const render = this.#host.tools.resolve(restored.name)?.render;
|
|
1033
|
+
if (render !== undefined) {
|
|
1034
|
+
this.#host.transcript.renderToolOutput(render, restored);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if (message.role === "activity") {
|
|
1040
|
+
// The client materialises a pushed activity as a message of its own, so a
|
|
1041
|
+
// chart's data is in the transcript already and survives a reload. Only
|
|
1042
|
+
// the drawing had to be put back.
|
|
1043
|
+
const activity = message as unknown as { activityType?: unknown; content?: unknown };
|
|
1044
|
+
if (typeof activity.activityType === "string") {
|
|
1045
|
+
this.#host.activities.draw(message.id, activity.activityType, activity.content);
|
|
1046
|
+
}
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
if (message.role === "tool") {
|
|
1050
|
+
const card = this.#host.transcript.card(message.toolCallId);
|
|
1051
|
+
if (card !== undefined) {
|
|
1052
|
+
// The outcome in the message's metadata -- folded there from a server's
|
|
1053
|
+
// result by `@ag-ui/client`, or written there by `AgUiClient` for a
|
|
1054
|
+
// result it made itself -- read back through the same mapping the live
|
|
1055
|
+
// path uses, so a card that said "declined" before the reload still
|
|
1056
|
+
// says it after. History stored by an earlier release has it at the
|
|
1057
|
+
// top level instead, which is read when the metadata has none. Narrowed
|
|
1058
|
+
// off `unknown` rather than trusted, like every other field read out of
|
|
1059
|
+
// the store: metadata is open by key, a host store may not round-trip
|
|
1060
|
+
// it, and history written before outcomes existed has none. All three
|
|
1061
|
+
// land on DONE, which is what this line did unconditionally.
|
|
1062
|
+
card.settle(
|
|
1063
|
+
toolStatusFromOutcome(metadataOrTopLevel(message, "outcome")),
|
|
1064
|
+
// Flattened exactly as the live path flattens it, so a reload shows
|
|
1065
|
+
// the card the user watched settle.
|
|
1066
|
+
contentToText(message.content),
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/** Parse a tool call's JSON `arguments` from history into an object. */
|
|
1073
|
+
#parseArgs(raw: unknown): Record<string, unknown> {
|
|
1074
|
+
if (typeof raw !== "string") {
|
|
1075
|
+
// A restored call whose `arguments` are missing or not a string still has
|
|
1076
|
+
// a name worth showing, so this renders an empty-args card rather than
|
|
1077
|
+
// dropping the card.
|
|
1078
|
+
return {};
|
|
1079
|
+
}
|
|
1080
|
+
try {
|
|
1081
|
+
const parsed: unknown = JSON.parse(raw);
|
|
1082
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
1083
|
+
return parsed as Record<string, unknown>;
|
|
1084
|
+
}
|
|
1085
|
+
} catch {
|
|
1086
|
+
// Malformed history — fall back to empty args rather than failing replay.
|
|
1087
|
+
}
|
|
1088
|
+
return {};
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Complete the checkpointed navigating tool call and continue the run.
|
|
1093
|
+
*
|
|
1094
|
+
* The call's card is settled here, from the result the continuation carries.
|
|
1095
|
+
* The restore left it open on purpose -- it is the one call a reload was
|
|
1096
|
+
* expected by -- and nothing after this answers it: the run that follows
|
|
1097
|
+
* streams no result for a call the client answered itself. Left open, it
|
|
1098
|
+
* spun through the whole continuation, and the sweep that closes a run then
|
|
1099
|
+
* found it pending and called it not finished -- on the one call known to
|
|
1100
|
+
* have finished, since the page it moved to is the page doing the asking.
|
|
1101
|
+
*
|
|
1102
|
+
* Settled from the string the stored message holds, and with no outcome,
|
|
1103
|
+
* since `addToolResult` writes none, so the card is the one a later reload
|
|
1104
|
+
* draws from that message. The card can be missing, when the stored
|
|
1105
|
+
* transcript does not hold the call -- a store that kept the checkpoint and
|
|
1106
|
+
* lost the turn that made it -- and then there is nothing to settle. That
|
|
1107
|
+
* guard is held by "resumes a checkpointed navigating tool call on mount" in
|
|
1108
|
+
* `ag_ui_chat.test.ts`, whose transcript never made the call.
|
|
1109
|
+
*/
|
|
1110
|
+
async #resumeFrom(checkpoint: NavigationCheckpoint, generation: number): Promise<void> {
|
|
1111
|
+
this.#host.conversationStore().saveCheckpoint(this.#threadId, null);
|
|
1112
|
+
const client = this.#host.ensureClient();
|
|
1113
|
+
// Called on the element, as `this.navigationResult(...)` always was.
|
|
1114
|
+
const landed = JSON.stringify(
|
|
1115
|
+
this.#host.navigationResult().call(this.#host.element, checkpoint),
|
|
1116
|
+
);
|
|
1117
|
+
// The store's save and `navigationResult` are host code, and either can
|
|
1118
|
+
// have stopped the restore. Going on answered the call and resumed anyway:
|
|
1119
|
+
// on the client taken before `navigationResult` ran, which nothing held any
|
|
1120
|
+
// more, with its tools still driving the page -- or, stopped by the save, on
|
|
1121
|
+
// the new chat's own client, which the line above had just built.
|
|
1122
|
+
if (!this.#restoring(generation)) {
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
client.addToolResult(checkpoint.toolCallId, landed);
|
|
1126
|
+
this.#host.transcript.card(checkpoint.toolCallId)?.settle(TOOL_CALL_STATUS.DONE, landed);
|
|
1127
|
+
await client.resume();
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/** One tool call as a restored assistant message carries it. */
|
|
1132
|
+
interface RestoredToolCall {
|
|
1133
|
+
readonly id: string;
|
|
1134
|
+
readonly function: { readonly name: string; readonly arguments?: unknown };
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* The tool calls a restored assistant turn carries, with anything shapeless dropped.
|
|
1139
|
+
*
|
|
1140
|
+
* Narrowing here rather than trusting the declared type, for three reasons that
|
|
1141
|
+
* point the same way.
|
|
1142
|
+
*
|
|
1143
|
+
* **`null` is a value this field really takes.** `@ag-ui/core` types `toolCalls`
|
|
1144
|
+
* as optional (`z.ZodOptional`), so TypeScript offers only `undefined` — but the
|
|
1145
|
+
* protocol's Python models declare `tool_calls: list[ToolCall] | None`, and a
|
|
1146
|
+
* server dumping them without `exclude_none` sends `null`. The two SDKs disagree
|
|
1147
|
+
* about the wire, and a client cannot afford to take either one's word for it.
|
|
1148
|
+
*
|
|
1149
|
+
* **A throw here costs the rest of the transcript.** This runs inside the replay
|
|
1150
|
+
* of stored history, one message at a time; an exception aborts the whole replay,
|
|
1151
|
+
* so a single bad entry silently truncates the conversation from that point on —
|
|
1152
|
+
* with no error state and nothing on screen to explain the gap.
|
|
1153
|
+
*
|
|
1154
|
+
* **Storage is untrusted anyway** — hand-edited, truncated, written by an older
|
|
1155
|
+
* version, or supplied by a host's own store. `messageAttachments` already takes
|
|
1156
|
+
* exactly this stance for the neighbouring field on the same message.
|
|
1157
|
+
*/
|
|
1158
|
+
function restoredToolCalls(value: unknown): readonly RestoredToolCall[] {
|
|
1159
|
+
return Array.isArray(value) ? value.filter(isRestoredToolCall) : [];
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/** Whether an unknown history entry has enough shape to render a tool card. */
|
|
1163
|
+
function isRestoredToolCall(value: unknown): value is RestoredToolCall {
|
|
1164
|
+
if (typeof value !== "object" || value === null) {
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1167
|
+
const call = value as { id?: unknown; function?: { name?: unknown } };
|
|
1168
|
+
return typeof call.id === "string" && typeof call.function?.name === "string";
|
|
1169
|
+
}
|