collavre 0.24.0 → 0.25.0
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.
- checksums.yaml +4 -4
- data/README.md +0 -1
- data/app/assets/stylesheets/collavre/comments_popup.css +165 -99
- data/app/assets/stylesheets/collavre/creatives.css +346 -37
- data/app/assets/stylesheets/collavre/desktop_setup.css +246 -0
- data/app/assets/stylesheets/collavre/gnb.css +64 -3
- data/app/assets/stylesheets/collavre/landing.css +108 -3
- data/app/assets/stylesheets/collavre/mention_menu.css +32 -0
- data/app/assets/stylesheets/collavre/popup.css +14 -0
- data/app/assets/stylesheets/collavre/print.css +0 -1
- data/app/assets/stylesheets/collavre/search_popup.css +1 -1
- data/app/assets/stylesheets/collavre/secret_fields.css +3 -0
- data/app/assets/stylesheets/collavre/workspace.css +307 -0
- data/app/channels/collavre/agent_channel.rb +14 -21
- data/app/channels/collavre/comments_presence_channel.rb +181 -21
- data/app/channels/collavre/inbox_badge_channel.rb +7 -1
- data/app/components/collavre/inbox/badge_component.rb +6 -13
- data/app/components/collavre/progress_filter_component.html.erb +2 -2
- data/app/components/collavre/progress_filter_component.rb +20 -0
- data/app/controllers/collavre/admin/settings_controller.rb +4 -1
- data/app/controllers/collavre/agent_connections_controller.rb +165 -0
- data/app/controllers/collavre/agent_gateways_controller.rb +132 -0
- data/app/controllers/collavre/agent_provisioning_controller.rb +79 -0
- data/app/controllers/collavre/api/v1/agents_controller.rb +45 -97
- data/app/controllers/collavre/application_controller.rb +13 -2
- data/app/controllers/collavre/comment_read_pointers_controller.rb +69 -40
- data/app/controllers/collavre/comments_controller.rb +45 -131
- data/app/controllers/collavre/concerns/shareable.rb +9 -3
- data/app/controllers/collavre/creatives_controller.rb +144 -11
- data/app/controllers/collavre/desktop_gateway_registrations_controller.rb +173 -0
- data/app/controllers/collavre/desktop_setup_controller.rb +119 -0
- data/app/controllers/collavre/desktop_sidecar_health_controller.rb +35 -0
- data/app/controllers/collavre/features_controller.rb +57 -0
- data/app/controllers/collavre/invites_controller.rb +12 -1
- data/app/controllers/collavre/llm_models_controller.rb +8 -0
- data/app/controllers/collavre/notices_controller.rb +20 -0
- data/app/controllers/collavre/tasks_controller.rb +40 -6
- data/app/controllers/collavre/topics_controller.rb +130 -53
- data/app/controllers/collavre/user_creative_preferences_controller.rb +214 -30
- data/app/controllers/concerns/collavre/comments/batch_operations.rb +15 -3
- data/app/controllers/concerns/collavre/comments/conversion.rb +1 -1
- data/app/controllers/concerns/collavre/users_controller/ai_user_management.rb +118 -8
- data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +1 -15
- data/app/controllers/concerns/collavre/users_controller/registration.rb +2 -2
- data/app/errors/collavre/turn_deadline_error.rb +20 -0
- data/app/helpers/collavre/application_helper.rb +38 -0
- data/app/helpers/collavre/creatives_helper.rb +56 -22
- data/app/helpers/collavre/navigation_helper.rb +12 -0
- data/app/javascript/collavre.js +1 -2
- data/app/javascript/components/InlineLexicalEditor.jsx +30 -21
- data/app/javascript/components/__tests__/creative_tree_row_parent_id.test.js +32 -2
- data/app/javascript/components/__tests__/creative_tree_row_progress_toggle.test.js +185 -0
- data/app/javascript/components/creative_tree_row.js +62 -22
- data/app/javascript/controllers/__tests__/agent_connection_controller.test.js +195 -0
- data/app/javascript/controllers/__tests__/desktop_proxy_setup_controller.test.js +96 -0
- data/app/javascript/controllers/__tests__/index.test.js +42 -0
- data/app/javascript/controllers/__tests__/last_visited_creative_controller.test.js +103 -0
- data/app/javascript/controllers/__tests__/link_creative_controller.test.js +253 -2
- data/app/javascript/controllers/__tests__/progress_filter_controller.test.js +134 -0
- data/app/javascript/controllers/__tests__/search_popup_controller.test.js +310 -0
- data/app/javascript/controllers/__tests__/topic_list_controller.test.js +111 -0
- data/app/javascript/controllers/__tests__/workspace_tree_controller.test.js +1011 -0
- data/app/javascript/controllers/agent_connection_controller.js +357 -0
- data/app/javascript/controllers/agent_vendor_controller.js +23 -0
- data/app/javascript/controllers/comment_controller.js +3 -1
- data/app/javascript/controllers/comments/__tests__/feature_cards_controller.test.js +223 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_default_placeholder.test.js +83 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_draft.test.js +2069 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_ime_send.test.js +324 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_review.test.js +28 -2
- data/app/javascript/controllers/comments/__tests__/form_controller_search_submit.test.js +106 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft.test.js +134 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft_creative_switch.test.js +166 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft_failure.test.js +183 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_deep_link_topic.test.js +467 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_docked.test.js +115 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_prev_message.test.js +245 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_read_pointer.test.js +276 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_stream_archived.test.js +221 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_stream_search.test.js +104 -0
- data/app/javascript/controllers/comments/__tests__/placeholder_controller.test.js +79 -0
- data/app/javascript/controllers/comments/__tests__/popup_controller.test.js +754 -1
- data/app/javascript/controllers/comments/__tests__/presence_controller.test.js +717 -15
- data/app/javascript/controllers/comments/__tests__/prev_message_navigator.test.js +129 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_action_errors.test.js +184 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_archived.test.js +879 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_creation_container.test.js +154 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_creation_scroll.test.js +92 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_delete.test.js +130 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_docked.test.js +32 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_last_topic_echo.test.js +1938 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_primary_agent.test.js +256 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_scroll.test.js +182 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_selection_race.test.js +981 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_topic_list.test.js +313 -0
- data/app/javascript/controllers/comments/feature_cards_controller.js +83 -0
- data/app/javascript/controllers/comments/form_controller.js +795 -14
- data/app/javascript/controllers/comments/list_controller.js +338 -52
- data/app/javascript/controllers/comments/placeholder_controller.js +30 -0
- data/app/javascript/controllers/comments/popup_controller.js +425 -83
- data/app/javascript/controllers/comments/presence_controller.js +317 -55
- data/app/javascript/controllers/comments/prev_message_navigator.js +94 -0
- data/app/javascript/controllers/comments/topics_controller.js +1772 -100
- data/app/javascript/controllers/common_popup_controller.js +2 -2
- data/app/javascript/controllers/creatives/__tests__/select_mode_controller.test.js +139 -0
- data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +605 -0
- data/app/javascript/controllers/creatives/__tests__/write_access_request_controller.test.js +125 -0
- data/app/javascript/controllers/creatives/select_mode_controller.js +7 -2
- data/app/javascript/controllers/creatives/tree_controller.js +140 -31
- data/app/javascript/controllers/creatives/write_access_request_controller.js +58 -0
- data/app/javascript/controllers/desktop_proxy_setup_controller.js +51 -0
- data/app/javascript/controllers/gateway_check_controller.js +36 -0
- data/app/javascript/controllers/index.js +25 -1
- data/app/javascript/controllers/last_visited_creative_controller.js +58 -0
- data/app/javascript/controllers/link_creative_controller.js +89 -18
- data/app/javascript/controllers/progress_filter_controller.js +12 -22
- data/app/javascript/controllers/search_popup_controller.js +64 -46
- data/app/javascript/controllers/share_user_search_controller.js +2 -2
- data/app/javascript/controllers/topic_list_controller.js +44 -7
- data/app/javascript/controllers/workspace_tree_controller.js +662 -0
- data/app/javascript/creatives/__tests__/tree_renderer.test.js +80 -0
- data/app/javascript/creatives/drag_drop/__tests__/drop_completion_empty_state.test.js +98 -0
- data/app/javascript/creatives/drag_drop/__tests__/topic_move_released_agent.test.js +125 -0
- data/app/javascript/creatives/drag_drop/event_handlers.js +12 -0
- data/app/javascript/creatives/tree_renderer.js +65 -22
- data/app/javascript/lib/__tests__/chat_drafts.test.js +1458 -0
- data/app/javascript/lib/__tests__/common_popup_bounds.test.js +8 -3
- data/app/javascript/lib/__tests__/common_popup_placement.test.js +374 -0
- data/app/javascript/lib/__tests__/common_popup_reshow.test.js +131 -0
- data/app/javascript/lib/__tests__/creative_link_prefetch.test.js +46 -0
- data/app/javascript/lib/__tests__/last_visited_creative.test.js +87 -0
- data/app/javascript/lib/__tests__/skill_html_entities.test.js +23 -0
- data/app/javascript/lib/__tests__/turbo_stream_actions.test.js +190 -0
- data/app/javascript/lib/api/__tests__/creatives.test.js +68 -0
- data/app/javascript/lib/api/__tests__/csrf_fetch.test.js +27 -0
- data/app/javascript/lib/api/__tests__/topics_save_last_topic.test.js +112 -0
- data/app/javascript/lib/api/creatives.js +32 -13
- data/app/javascript/lib/api/csrf_fetch.js +6 -4
- data/app/javascript/lib/api/topics.js +63 -5
- data/app/javascript/lib/chat_drafts.js +786 -0
- data/app/javascript/lib/common_popup.js +164 -25
- data/app/javascript/lib/creative_link_prefetch.js +27 -0
- data/app/javascript/lib/last_visited_creative.js +97 -0
- data/app/javascript/lib/lexical/__tests__/creative_link_node.test.js +162 -0
- data/app/javascript/lib/lexical/__tests__/creative_link_trigger.test.js +133 -0
- data/app/javascript/lib/lexical/__tests__/link_toolbar.test.js +62 -0
- data/app/javascript/lib/lexical/creative_link_node.js +101 -0
- data/app/javascript/lib/lexical/creative_link_trigger.js +98 -0
- data/app/javascript/lib/lexical/link_toolbar.js +23 -0
- data/app/javascript/lib/turbo_stream_actions.js +23 -16
- data/app/javascript/lib/utils/__tests__/filter_navigation.test.js +210 -0
- data/app/javascript/lib/utils/__tests__/markdown.test.js +44 -0
- data/app/javascript/lib/utils/filter_navigation.js +100 -0
- data/app/javascript/modules/__tests__/command_menu_creative_picker.test.js +62 -0
- data/app/javascript/modules/__tests__/command_menu_typeahead.test.js +267 -0
- data/app/javascript/modules/__tests__/creative_link_picker.test.js +85 -0
- data/app/javascript/modules/__tests__/creative_row_editor_archive.test.js +391 -0
- data/app/javascript/modules/__tests__/creative_row_editor_delegated_clicks.test.js +158 -0
- data/app/javascript/modules/__tests__/creative_row_editor_empty_state_recovery.test.js +519 -0
- data/app/javascript/modules/__tests__/creative_row_editor_empty_state_removal.test.js +346 -0
- data/app/javascript/modules/__tests__/creative_row_editor_session.test.js +144 -0
- data/app/javascript/modules/__tests__/creative_tree_dom.test.js +30 -0
- data/app/javascript/modules/__tests__/creative_tree_empty_state.test.js +186 -0
- data/app/javascript/modules/__tests__/dom_listener_registry.test.js +67 -0
- data/app/javascript/modules/__tests__/support/inline_editor_dom.js +159 -0
- data/app/javascript/modules/command_args_form.js +0 -12
- data/app/javascript/modules/command_menu.js +80 -38
- data/app/javascript/modules/creative_link_picker.js +56 -0
- data/app/javascript/modules/creative_row_editor.js +446 -243
- data/app/javascript/modules/creative_row_editor_delegated_clicks.js +87 -0
- data/app/javascript/modules/creative_tree_dom.js +4 -0
- data/app/javascript/modules/creative_tree_empty_state.js +80 -0
- data/app/javascript/modules/dom_listener_registry.js +23 -0
- data/app/javascript/modules/mention_menu.js +2 -3
- data/app/javascript/utils/__tests__/caret_anchor.test.js +75 -0
- data/app/javascript/utils/__tests__/chevron_icons.test.js +15 -0
- data/app/javascript/utils/caret_position.js +10 -0
- data/app/javascript/utils/chevron_icons.js +7 -0
- data/app/jobs/collavre/ai_agent_job.rb +262 -72
- data/app/jobs/collavre/cancel_offline_delegated_tasks_job.rb +11 -36
- data/app/jobs/collavre/comment_badges_broadcast_job.rb +28 -0
- data/app/jobs/collavre/comment_link_preview_job.rb +33 -0
- data/app/jobs/collavre/comment_notification_job.rb +14 -0
- data/app/jobs/collavre/comment_push_delivery_sweep_job.rb +17 -0
- data/app/jobs/collavre/compress_job.rb +33 -22
- data/app/jobs/collavre/creative_broadcast_job.rb +161 -48
- data/app/jobs/collavre/cron_action_job.rb +36 -18
- data/app/jobs/collavre/drop_trigger_job.rb +23 -9
- data/app/jobs/collavre/merge_comments_job.rb +33 -16
- data/app/jobs/collavre/push_notification_job.rb +62 -18
- data/app/jobs/collavre/restore_dropped_dispatches_job.rb +17 -0
- data/app/jobs/collavre/stuck_detector_job.rb +1 -1
- data/app/jobs/collavre/touch_creative_subtree_job.rb +14 -0
- data/app/models/collavre/agent_gateway.rb +200 -0
- data/app/models/collavre/agent_subscription.rb +4 -4
- data/app/models/collavre/agent_workspace.rb +210 -0
- data/app/models/collavre/comment/broadcastable.rb +22 -52
- data/app/models/collavre/comment/notifiable.rb +154 -77
- data/app/models/collavre/comment/topic_membership.rb +41 -0
- data/app/models/collavre/comment.rb +336 -31
- data/app/models/collavre/comment_notification_delivery.rb +61 -0
- data/app/models/collavre/comment_presence_store/cache_batches.rb +64 -0
- data/app/models/collavre/comment_presence_store.rb +272 -12
- data/app/models/collavre/comment_read_pointer.rb +9 -15
- data/app/models/collavre/creative/describable.rb +8 -2
- data/app/models/collavre/creative/permissible.rb +11 -0
- data/app/models/collavre/creative.rb +29 -4
- data/app/models/collavre/creative_share.rb +109 -4
- data/app/models/collavre/current.rb +1 -0
- data/app/models/collavre/llm_model.rb +62 -0
- data/app/models/collavre/system_setting.rb +12 -1
- data/app/models/collavre/task.rb +121 -2
- data/app/models/collavre/topic.rb +51 -0
- data/app/models/collavre/user.rb +101 -41
- data/app/models/collavre/user_creative_preference.rb +3 -1
- data/app/services/collavre/agent_provisioning/archive.rb +108 -0
- data/app/services/collavre/ai_agent/a2a_dispatcher.rb +16 -3
- data/app/services/collavre/ai_agent/agent_lifecycle_manager.rb +63 -6
- data/app/services/collavre/ai_agent/approval_handler.rb +12 -4
- data/app/services/collavre/ai_agent/claude_channel_adapter.rb +10 -5
- data/app/services/collavre/ai_agent/merged_trigger_comments.rb +164 -0
- data/app/services/collavre/ai_agent/message_builder.rb +103 -16
- data/app/services/collavre/ai_agent/session_provisioner.rb +3 -7
- data/app/services/collavre/ai_agent/task_claim_service.rb +29 -15
- data/app/services/collavre/ai_agent/task_reply_service.rb +67 -0
- data/app/services/collavre/ai_agent_service.rb +108 -20
- data/app/services/collavre/ai_client.rb +211 -21
- data/app/services/collavre/cli_proxy/client.rb +147 -0
- data/app/services/collavre/cli_proxy/endpoint_policy.rb +81 -0
- data/app/services/collavre/cli_proxy/identity.rb +47 -0
- data/app/services/collavre/cli_proxy/safe_net_http_adapter.rb +19 -0
- data/app/services/collavre/command_menu_service.rb +0 -10
- data/app/services/collavre/comment_move_service.rb +96 -7
- data/app/services/collavre/comment_snapshot_restore_service.rb +11 -5
- data/app/services/collavre/comments/action_executor.rb +48 -20
- data/app/services/collavre/comments/command_processor.rb +0 -1
- data/app/services/collavre/comments/list_response.rb +133 -0
- data/app/services/collavre/comments/read_pointer_writer.rb +210 -0
- data/app/services/collavre/comments/read_receipt_broadcaster.rb +157 -0
- data/app/services/collavre/comments/read_receipt_index.rb +152 -0
- data/app/services/collavre/comments/topic_command.rb +72 -1
- data/app/services/collavre/comments/topic_mutation.rb +20 -0
- data/app/services/collavre/creatives/breadcrumb_resolver.rb +1 -1
- data/app/services/collavre/creatives/comment_badge_index.rb +299 -27
- data/app/services/collavre/creatives/reorderer.rb +15 -3
- data/app/services/collavre/creatives/tree_builder.rb +1 -1
- data/app/services/collavre/creatives/tree_formatter.rb +5 -6
- data/app/services/collavre/creatives/workspace_path_resolver.rb +50 -0
- data/app/services/collavre/creatives/workspace_tree_builder.rb +92 -0
- data/app/services/collavre/crons/recurring_topic_tasks.rb +23 -0
- data/app/services/collavre/home_page_path.rb +55 -0
- data/app/services/collavre/html_text.rb +64 -0
- data/app/services/collavre/http_client.rb +11 -2
- data/app/services/collavre/inbox_reply_service.rb +1 -1
- data/app/services/collavre/markdown_converter.rb +13 -1
- data/app/services/collavre/orchestration/agent_orchestrator.rb +776 -81
- data/app/services/collavre/orchestration/arbiter.rb +48 -5
- data/app/services/collavre/orchestration/deferred_trigger_scope.rb +53 -0
- data/app/services/collavre/orchestration/delivery_record.rb +792 -0
- data/app/services/collavre/orchestration/loop_breaker.rb +10 -4
- data/app/services/collavre/orchestration/matcher.rb +132 -17
- data/app/services/collavre/orchestration/policy_resolver.rb +58 -10
- data/app/services/collavre/orchestration/scheduler.rb +10 -4
- data/app/services/collavre/orchestration/scheduling_hooks.rb +10 -0
- data/app/services/collavre/orchestration/selection.rb +43 -0
- data/app/services/collavre/orchestration/stuck_detector.rb +25 -112
- data/app/services/collavre/orchestration/task_coalescer.rb +355 -0
- data/app/services/collavre/orchestration/topic_slot.rb +73 -0
- data/app/services/collavre/system_events/context_builder.rb +60 -11
- data/app/services/collavre/system_events/dispatcher.rb +58 -12
- data/app/services/collavre/system_events/envelope.rb +73 -0
- data/app/services/collavre/system_events/vocabulary.rb +45 -0
- data/app/services/collavre/tools/creative_retrieval_service.rb +1 -1
- data/app/services/collavre/tools/cron_create_service.rb +32 -18
- data/app/services/collavre/tools/id_list.rb +38 -0
- data/app/services/collavre/tools/topic_authorizer.rb +61 -7
- data/app/services/collavre/tools/topic_branch_service.rb +85 -0
- data/app/services/collavre/tools/topic_create_service.rb +117 -0
- data/app/services/collavre/tools/topic_list_service.rb +101 -0
- data/app/services/collavre/tools/topic_message_create_service.rb +192 -0
- data/app/services/collavre/tools/topic_messages_service.rb +346 -0
- data/app/services/collavre/tools/topic_move_service.rb +108 -0
- data/app/services/collavre/tools/topic_selection.rb +59 -0
- data/app/services/collavre/tools/topic_update_service.rb +165 -0
- data/app/services/collavre/topic_branch_service.rb +39 -10
- data/app/services/collavre/topics/agent_resolver.rb +87 -0
- data/app/services/collavre/topics/char_budget.rb +70 -0
- data/app/services/collavre/topics/last_topic_preference_payload.rb +18 -0
- data/app/services/collavre/topics/message_page.rb +380 -0
- data/app/services/collavre/topics/message_page_markdown.rb +102 -0
- data/app/services/collavre/topics/message_scope.rb +61 -0
- data/app/services/collavre/topics/message_stats.rb +92 -0
- data/app/services/collavre/topics/move_blocker.rb +47 -0
- data/app/services/collavre/topics/next_name.rb +35 -0
- data/app/services/collavre/topics/pin_rejection.rb +25 -0
- data/app/services/collavre/topics/read_pointer_relocator.rb +71 -0
- data/app/services/collavre/topics/reserved_name.rb +18 -0
- data/app/services/collavre/topics/serializer.rb +92 -0
- data/app/services/collavre/topics/topic_destroy.rb +30 -0
- data/app/services/collavre/topics/topic_move.rb +80 -0
- data/app/services/collavre/topics/topic_move_effects.rb +35 -0
- data/app/services/collavre/topics/topic_mutation.rb +27 -0
- data/app/views/collavre/admin/integrations/_setting_row.html.erb +5 -8
- data/app/views/collavre/admin/settings/_system_tab.html.erb +7 -0
- data/app/views/collavre/agent_connections/show.html.erb +77 -0
- data/app/views/collavre/agent_gateways/_form.html.erb +71 -0
- data/app/views/collavre/agent_gateways/edit.html.erb +2 -0
- data/app/views/collavre/agent_gateways/index.html.erb +54 -0
- data/app/views/collavre/agent_gateways/new.html.erb +2 -0
- data/app/views/collavre/comments/_comments_popup.html.erb +32 -17
- data/app/views/collavre/comments/_empty_state.html.erb +33 -0
- data/app/views/collavre/comments/_empty_state_minimal.html.erb +7 -0
- data/app/views/collavre/comments/_feature_card.html.erb +35 -0
- data/app/views/collavre/comments/_list.html.erb +3 -2
- data/app/views/collavre/comments/_no_search_results.html.erb +3 -0
- data/app/views/collavre/creatives/_add_button.html.erb +17 -19
- data/app/views/collavre/creatives/_empty_state.html.erb +109 -0
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +5 -5
- data/app/views/collavre/creatives/_tree_loading_placeholder.html.erb +16 -0
- data/app/views/collavre/creatives/_workspace_tree.html.erb +33 -0
- data/app/views/collavre/creatives/index.html.erb +79 -18
- data/app/views/collavre/desktop_setup/show.html.erb +114 -0
- data/app/views/collavre/features/index.html.erb +42 -0
- data/app/views/collavre/features/show.html.erb +52 -0
- data/app/views/collavre/invitation_mailer/invite.html.erb +1 -1
- data/app/views/collavre/invitation_mailer/invite.text.erb +1 -1
- data/app/views/collavre/invites/show.html.erb +14 -1
- data/app/views/collavre/landing/show.html.erb +5 -1
- data/app/views/collavre/shared/_link_creative_modal.html.erb +4 -1
- data/app/views/collavre/shared/navigation/_help_button.html.erb +3 -1
- data/app/views/collavre/shared/navigation/_panels.html.erb +1 -8
- data/app/views/collavre/shared/navigation/_search_form.html.erb +13 -2
- data/app/views/collavre/user_themes/index.html.erb +1 -1
- data/app/views/collavre/users/_llm_fields.html.erb +65 -0
- data/app/views/collavre/users/_org_chart_node.html.erb +1 -1
- data/app/views/collavre/users/edit_ai.html.erb +33 -31
- data/app/views/collavre/users/new.html.erb +4 -2
- data/app/views/collavre/users/new_ai.html.erb +14 -32
- data/app/views/collavre/users/show.html.erb +13 -1
- data/app/views/layouts/collavre/landing.html.erb +9 -4
- data/config/initializers/ruby_llm_openai_reasoning_content.rb +77 -0
- data/config/locales/admin.en.yml +3 -1
- data/config/locales/admin.ko.yml +3 -1
- data/config/locales/agent_gateway.en.yml +217 -0
- data/config/locales/agent_gateway.ko.yml +217 -0
- data/config/locales/ai_agent.en.yml +0 -1
- data/config/locales/ai_agent.ko.yml +0 -1
- data/config/locales/comments.en.yml +72 -30
- data/config/locales/comments.ko.yml +72 -30
- data/config/locales/creatives.en.yml +32 -1
- data/config/locales/creatives.ko.yml +32 -1
- data/config/locales/features.en.yml +140 -0
- data/config/locales/features.ko.yml +140 -0
- data/config/locales/invites.en.yml +3 -0
- data/config/locales/invites.ko.yml +3 -0
- data/config/locales/landing.en.yml +2 -1
- data/config/locales/landing.ko.yml +2 -1
- data/config/locales/tools.en.yml +20 -0
- data/config/locales/tools.ko.yml +20 -0
- data/config/locales/users.en.yml +15 -14
- data/config/locales/users.ko.yml +14 -14
- data/config/routes.rb +45 -19
- data/db/migrate/20250911084338_backfill_creative_in_comment_inbox_items.rb +9 -1
- data/db/migrate/20260724000000_add_creative_id_index_to_comments.rb +34 -0
- data/db/migrate/20260724000001_add_creative_private_id_index_to_comments.rb +50 -0
- data/db/migrate/20260724090000_add_async_comment_notification_state.rb +64 -0
- data/db/migrate/20260727000000_add_waiting_notice_scope_to_comments.rb +30 -0
- data/db/migrate/20260727010000_add_waiting_notice_scope_to_tasks.rb +26 -0
- data/db/migrate/20260805000000_add_creative_workspace_enabled_to_users.rb +5 -0
- data/db/migrate/20260807050000_add_dismissed_notices_to_users.rb +5 -0
- data/db/migrate/20260809000000_change_creative_workspace_enabled_default_to_true.rb +18 -0
- data/db/migrate/20260809000001_create_llm_models.rb +64 -0
- data/db/migrate/20260809000002_create_agent_gateways_and_workspaces.rb +35 -0
- data/db/migrate/20260809000003_hash_agent_workspace_callback_tokens.rb +44 -0
- data/db/migrate/20260809000004_encrypt_agent_workspace_manifest_tokens.rb +59 -0
- data/db/migrate/20260810000000_split_agent_workspace_identity_axes.rb +58 -0
- data/db/migrate/20260810000001_seed_codex_custom_proxy_llm_models.rb +28 -0
- data/db/migrate/20260810010000_add_last_visited_creative_to_users.rb +8 -0
- data/db/migrate/20260810020000_add_last_visited_creative_at_to_users.rb +5 -0
- data/db/migrate/20260810030000_add_last_visited_creative_navigation_order_to_users.rb +6 -0
- data/db/migrate/20260810060000_add_last_visited_creative_issued_sequence_to_users.rb +5 -0
- data/db/migrate/20260810100000_add_desktop_managed_to_agent_gateways.rb +40 -0
- data/db/migrate/20260810100001_add_desktop_preset_adapter_to_users.rb +38 -0
- data/db/migrate/20260811000000_make_agent_gateway_completion_key_nullable.rb +7 -0
- data/db/migrate/20260812000000_remove_workflow_columns_from_tasks.rb +101 -0
- data/db/migrate/20260812000001_remove_android_devices.rb +24 -0
- data/db/migrate/20260812010000_drop_inbox_items.rb +22 -0
- data/db/migrate/20260813000000_clear_claude_channel_presence_routing_expressions.rb +16 -0
- data/db/migrate/20260813000001_add_last_topic_revision_to_user_creative_preferences.rb +5 -0
- data/db/migrate/20260813000002_add_last_topic_save_order_to_user_creative_preferences.rb +6 -0
- data/db/migrate/20260813000003_add_last_topic_save_sequences_to_user_creative_preferences.rb +5 -0
- data/db/migrate/20260813000004_add_last_topic_save_fences_to_user_creative_preferences.rb +6 -0
- data/db/migrate/20260814000000_add_topic_to_comment_read_pointers.rb +79 -0
- data/db/migrate/20260814000001_add_creative_topic_private_id_index_to_comments.rb +37 -0
- data/db/migrate/20260816000000_add_watermark_index_to_comment_read_pointers.rb +27 -0
- data/db/migrate/20260816000001_add_private_recipient_topic_index_to_comments.rb +48 -0
- data/db/migrate/20260820000000_add_topic_assigned_at_to_comments.rb +13 -0
- data/db/seeds.rb +1 -51
- data/lib/collavre/engine.rb +108 -1
- data/lib/collavre/feature_card_registry.rb +168 -0
- data/lib/collavre/hashed_access_token_lookup.rb +22 -0
- data/lib/collavre/sensitive_request_silencer.rb +23 -0
- data/lib/collavre/user_extensions.rb +0 -2
- data/lib/collavre/version.rb +1 -1
- data/lib/collavre.rb +1 -0
- data/skills/collavre/SKILL.md +165 -0
- data/skills/collavre/package.json +8 -0
- data/skills/collavre/references/tool-reference.md +347 -0
- data/skills/collavre/scripts/collavre +634 -0
- data/skills/collavre/scripts/html_entities.js +24 -0
- metadata +228 -30
- data/app/controllers/collavre/api/v1/mobile/agent_events_controller.rb +0 -224
- data/app/controllers/collavre/api/v1/mobile/base_controller.rb +0 -95
- data/app/controllers/collavre/api/v1/mobile/devices_controller.rb +0 -31
- data/app/controllers/collavre/api/v1/mobile/voice_commands_controller.rb +0 -25
- data/app/controllers/collavre/inbox_items_controller.rb +0 -70
- data/app/controllers/collavre/typo_corrections_controller.rb +0 -39
- data/app/javascript/lib/__tests__/typo_correction.test.js +0 -192
- data/app/javascript/lib/typo_correction.js +0 -146
- data/app/javascript/modules/__tests__/typo_corrector.test.js +0 -365
- data/app/javascript/modules/creative_guide.js +0 -67
- data/app/javascript/modules/typo_corrector.js +0 -534
- data/app/jobs/collavre/inbox_summary_job.rb +0 -24
- data/app/mailers/collavre/inbox_mailer.rb +0 -19
- data/app/models/collavre/inbox_item.rb +0 -89
- data/app/services/collavre/comments/concerns/workflow_support.rb +0 -115
- data/app/services/collavre/comments/work_command.rb +0 -161
- data/app/services/collavre/comments/workflow_executor.rb +0 -276
- data/app/services/collavre/gemini_parent_recommender.rb +0 -77
- data/app/services/collavre/mobile/event_summarizer.rb +0 -40
- data/app/services/collavre/typo_corrector.rb +0 -188
- data/app/views/collavre/inbox_items/_item.html.erb +0 -18
- data/app/views/collavre/inbox_items/_items.html.erb +0 -3
- data/app/views/collavre/inbox_items/_list.html.erb +0 -7
- data/app/views/collavre/inbox_items/index.html.erb +0 -1
- data/app/views/collavre/inbox_mailer/daily_summary.html.erb +0 -11
- data/app/views/collavre/inbox_mailer/daily_summary.text.erb +0 -8
- data/app/views/collavre/users/typo_correction.html.erb +0 -50
- data/config/locales/mobile.en.yml +0 -16
- data/config/locales/mobile.ko.yml +0 -16
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
module AiAgent
|
|
5
|
+
# Keeps a delegated Claude reply's claim, comment insert, and completion on
|
|
6
|
+
# the topic lock used by TopicMove. A move therefore sees the delegated task
|
|
7
|
+
# and waits, or runs after the complete reply is committed.
|
|
8
|
+
class TaskReplyService
|
|
9
|
+
Result = Data.define(:status, :body, :comment, :agent, :task)
|
|
10
|
+
|
|
11
|
+
def initialize(topic:, current_user:, text:, requested_task_id:, agent_resolver:, task_claimer:, claim_service:)
|
|
12
|
+
@topic = topic
|
|
13
|
+
@current_user = current_user
|
|
14
|
+
@text = text
|
|
15
|
+
@requested_task_id = requested_task_id
|
|
16
|
+
@agent_resolver = agent_resolver
|
|
17
|
+
@task_claimer = task_claimer
|
|
18
|
+
@claim_service = claim_service
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call
|
|
22
|
+
topic.with_lock { build_result }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
attr_reader :topic, :current_user, :text, :requested_task_id, :agent_resolver, :task_claimer, :claim_service
|
|
28
|
+
|
|
29
|
+
def build_result
|
|
30
|
+
creative = topic.creative&.effective_origin
|
|
31
|
+
return result(:not_found, error: "Creative not found") unless creative
|
|
32
|
+
return result(:forbidden, error: "Not authorized") unless creative.has_permission?(current_user, :feedback)
|
|
33
|
+
|
|
34
|
+
agent = agent_resolver.call(topic, requested_task_id)
|
|
35
|
+
return result(:forbidden, error: "Not authorized") unless agent
|
|
36
|
+
|
|
37
|
+
task = task_claimer.call(agent, topic, requested_task_id)
|
|
38
|
+
if requested_task_id.present? && task.nil?
|
|
39
|
+
return result(:conflict, error: "Task already completed or not delegated")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
persist_reply(creative, agent, task)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def persist_reply(creative, agent, task)
|
|
46
|
+
comment = creative.comments.build(
|
|
47
|
+
content: text.to_s, topic: topic, user: agent,
|
|
48
|
+
skip_default_user: true, skip_dispatch: true
|
|
49
|
+
)
|
|
50
|
+
return failed_result(comment, task) unless comment.save
|
|
51
|
+
|
|
52
|
+
claim_service.link_reply(task: task, comment: comment) if task
|
|
53
|
+
claim_service.finalize(agent: agent, task: task, comment: comment) if task
|
|
54
|
+
Result.new(status: :created, body: { comment_id: comment.id }, comment: comment, agent: agent, task: task)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def failed_result(comment, task)
|
|
58
|
+
task&.update!(status: "delegated")
|
|
59
|
+
result(:unprocessable_entity, errors: comment.errors.full_messages)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def result(status, **body)
|
|
63
|
+
Result.new(status: status, body: body, comment: nil, agent: nil, task: nil)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -13,25 +13,39 @@ module Collavre
|
|
|
13
13
|
@task = task
|
|
14
14
|
@agent = task.agent
|
|
15
15
|
@context = task.trigger_event_payload
|
|
16
|
+
@original_comment = find_original_comment
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def call
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
begin
|
|
21
|
+
Current.set(user: @agent, agent_turn: { user: workspace_user, task: @task }) do
|
|
22
|
+
if @agent.claude_channel_agent?
|
|
23
|
+
delegate_to_claude_channel
|
|
24
|
+
else
|
|
25
|
+
execute_llm_conversation
|
|
26
|
+
end
|
|
24
27
|
end
|
|
28
|
+
rescue ApprovalPendingError => e
|
|
29
|
+
summary = generate_approval_summary(e)
|
|
30
|
+
AiAgent::ApprovalHandler.new(
|
|
31
|
+
task: @task, agent: @agent, context: @context,
|
|
32
|
+
creative: @creative, reply_comment: @reply_comment
|
|
33
|
+
).handle(e, summary: summary)
|
|
34
|
+
raise
|
|
25
35
|
end
|
|
26
|
-
rescue
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
rescue TurnDeadlineError => e
|
|
37
|
+
handle_cancelled(
|
|
38
|
+
action_type: "failed",
|
|
39
|
+
message: "Turn exceeded the #{e.deadline_seconds}s deadline"
|
|
40
|
+
)
|
|
41
|
+
abort_agent_session_if_needed
|
|
32
42
|
raise
|
|
33
43
|
rescue CancelledError
|
|
34
|
-
|
|
44
|
+
if @task.reload.status == "failed"
|
|
45
|
+
handle_cancelled(action_type: "failed", message: "Task failed externally")
|
|
46
|
+
else
|
|
47
|
+
handle_cancelled
|
|
48
|
+
end
|
|
35
49
|
abort_agent_session_if_needed
|
|
36
50
|
raise
|
|
37
51
|
end
|
|
@@ -60,7 +74,6 @@ module Collavre
|
|
|
60
74
|
def execute_llm_conversation
|
|
61
75
|
log_action("start", { message: "Starting agent execution" })
|
|
62
76
|
|
|
63
|
-
@original_comment = find_original_comment
|
|
64
77
|
messages_data = build_messages
|
|
65
78
|
log_action("prompt_generated", { messages: messages_data[:messages] })
|
|
66
79
|
|
|
@@ -70,6 +83,14 @@ module Collavre
|
|
|
70
83
|
|
|
71
84
|
resolved = resolve_session_context(messages_data, system_prompt)
|
|
72
85
|
|
|
86
|
+
# Write down what this turn is about to hand the agent, before it hands it
|
|
87
|
+
# over. A comment that landed after this task was dispatched and got swept
|
|
88
|
+
# into the history window has been read by the agent, and the dispatch
|
|
89
|
+
# still on its way for it should be dropped rather than queued behind this
|
|
90
|
+
# turn. Recorded off `resolved` — after the session filter — because a
|
|
91
|
+
# session-backed agent is sent only its :trigger and swallows nothing.
|
|
92
|
+
Orchestration::DeliveryRecord.record!(@task, resolved)
|
|
93
|
+
|
|
73
94
|
@reply_comment = create_reply_comment_if_needed
|
|
74
95
|
|
|
75
96
|
@lifecycle_manager = AiAgent::AgentLifecycleManager.new(
|
|
@@ -86,12 +107,40 @@ module Collavre
|
|
|
86
107
|
@lifecycle_manager.broadcast_status("thinking")
|
|
87
108
|
|
|
88
109
|
@client = build_ai_client(resolved[:system_prompt])
|
|
89
|
-
|
|
110
|
+
begin
|
|
111
|
+
stream_response(@client, resolved)
|
|
112
|
+
ensure
|
|
113
|
+
# Write down that the payload got there, whatever became of the turn
|
|
114
|
+
# afterwards. In an `ensure` because the ending this is for leaves by
|
|
115
|
+
# exception: a user pressing Stop mid-answer raises CancelledError out
|
|
116
|
+
# of the block above, and the task ends `cancelled` — an undelivered
|
|
117
|
+
# ending to every reader, although the agent has read this turn's
|
|
118
|
+
# payload and every comment it swallowed. See
|
|
119
|
+
# Orchestration::DeliveryRecord::HANDED_OFF_KEY.
|
|
120
|
+
Orchestration::DeliveryRecord.mark_handed_off!(@task) if @client.handed_off?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# ...and write down when the handing over did not happen. The record
|
|
124
|
+
# above licences discarding other dispatches on the strength of the agent
|
|
125
|
+
# having read their comments; a request that never reached the provider
|
|
126
|
+
# read nothing. AiAgentJob finishes this task `done` either way, so
|
|
127
|
+
# without this the delivered ending is the one it ends in. See
|
|
128
|
+
# Orchestration::DeliveryRecord::HANDOFF_FAILED_KEY.
|
|
129
|
+
Orchestration::DeliveryRecord.mark_handoff_failed!(@task) if @client.last_handoff_failed?
|
|
130
|
+
|
|
131
|
+
# Signal thinking state during finalize so the indicator shows ⏳
|
|
132
|
+
@lifecycle_manager.broadcast_status("thinking")
|
|
90
133
|
|
|
91
134
|
log_action("completion", { response: @streamer.content })
|
|
92
135
|
|
|
93
136
|
finalized_comment = finalize_response
|
|
137
|
+
# Stop/failure can land while the finalizer persists and broadcasts the
|
|
138
|
+
# reply. Revalidate before dispatching that reply to other agents.
|
|
139
|
+
@lifecycle_manager.check_cancelled!(force: true)
|
|
94
140
|
dispatch_a2a(finalized_comment) unless @finalizer&.review_flow
|
|
141
|
+
# A2A dispatch may itself perform I/O. Keep the normal return path from
|
|
142
|
+
# handing a stale "success" to AiAgentJob after a terminal transition.
|
|
143
|
+
@lifecycle_manager.check_cancelled!(force: true)
|
|
95
144
|
|
|
96
145
|
@lifecycle_manager.broadcast_status("idle")
|
|
97
146
|
|
|
@@ -172,17 +221,35 @@ module Collavre
|
|
|
172
221
|
creative: @creative,
|
|
173
222
|
user: @agent,
|
|
174
223
|
task: @task,
|
|
175
|
-
comment: @reply_comment || @original_comment
|
|
176
|
-
|
|
224
|
+
comment: @reply_comment || @original_comment,
|
|
225
|
+
workspace_user: workspace_user
|
|
226
|
+
},
|
|
227
|
+
request_timeout_seconds: @lifecycle_manager.method(:remaining_deadline_seconds),
|
|
228
|
+
# The streaming block below checks cancellation only when a text delta
|
|
229
|
+
# arrives; a tool-only loop emits none, so the tool-call boundary is
|
|
230
|
+
# that loop's only checkpoint against terminal status and the deadline.
|
|
231
|
+
before_tool_call: ->(force = false) { @lifecycle_manager.check_cancelled!(force: force) }
|
|
177
232
|
)
|
|
178
233
|
end
|
|
179
234
|
|
|
180
235
|
def stream_response(client, messages_data)
|
|
181
|
-
|
|
236
|
+
# Prompt/session preparation can take long enough for Stop or
|
|
237
|
+
# StuckDetector to end the task before the provider request begins.
|
|
238
|
+
# Bypass the new manager's initial polling throttle at this handoff
|
|
239
|
+
# boundary so a terminal turn cannot start remote tool side effects.
|
|
240
|
+
@lifecycle_manager.check_cancelled!(force: true)
|
|
241
|
+
response = client.chat(messages_data, tools: @agent.tools || []) do |delta|
|
|
182
242
|
@lifecycle_manager.check_cancelled!
|
|
183
243
|
@streamer.append(delta)
|
|
184
244
|
@lifecycle_manager.heartbeat_if_needed
|
|
185
245
|
end
|
|
246
|
+
# A provider can return its final response inside the one-second polling
|
|
247
|
+
# throttle after StuckDetector failed the task or the turn crossed its
|
|
248
|
+
# deadline. This is the common completion boundary for RubyLLM and both
|
|
249
|
+
# OpenClaw transports; validate it without throttling before finalization
|
|
250
|
+
# can overwrite the terminal outcome.
|
|
251
|
+
@lifecycle_manager.check_cancelled!(force: true)
|
|
252
|
+
response
|
|
186
253
|
end
|
|
187
254
|
|
|
188
255
|
def finalize_response
|
|
@@ -202,15 +269,36 @@ module Collavre
|
|
|
202
269
|
dispatcher = AiAgent::A2aDispatcher.new(
|
|
203
270
|
agent: @agent,
|
|
204
271
|
reply_comment: finalized_comment,
|
|
205
|
-
context: @context
|
|
272
|
+
context: @context,
|
|
273
|
+
workspace_user: workspace_user
|
|
206
274
|
)
|
|
207
275
|
dispatcher.dispatch
|
|
208
276
|
end
|
|
209
277
|
|
|
210
|
-
def
|
|
278
|
+
def workspace_user
|
|
279
|
+
@workspace_user ||= begin
|
|
280
|
+
carried_principal = @context.key?("workspace_user_id")
|
|
281
|
+
carried_user = User.find_by(id: @context["workspace_user_id"])
|
|
282
|
+
comment_user = @original_comment&.user
|
|
283
|
+
|
|
284
|
+
if carried_user && !carried_user.ai_user?
|
|
285
|
+
carried_user
|
|
286
|
+
elsif carried_principal
|
|
287
|
+
nil
|
|
288
|
+
elsif comment_user && !comment_user.ai_user?
|
|
289
|
+
comment_user
|
|
290
|
+
else
|
|
291
|
+
@agent.creator
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def handle_cancelled(action_type: "cancelled", message: "Task cancelled by user")
|
|
211
297
|
@lifecycle_manager.handle_cancelled(
|
|
212
298
|
reply_comment: @reply_comment,
|
|
213
|
-
response_content: @streamer.content
|
|
299
|
+
response_content: @streamer.content,
|
|
300
|
+
action_type: action_type,
|
|
301
|
+
message: message
|
|
214
302
|
)
|
|
215
303
|
|
|
216
304
|
# Reassociate activity logs if there was partial content
|
|
@@ -10,13 +10,45 @@ module Collavre
|
|
|
10
10
|
|
|
11
11
|
attr_reader :last_input_tokens, :last_output_tokens
|
|
12
12
|
|
|
13
|
+
# Did the last #chat end without the provider ever receiving the payload?
|
|
14
|
+
#
|
|
15
|
+
# #chat swallows a provider error: it streams "⚠️ AI Error" to the caller
|
|
16
|
+
# and returns nil — which an ordinary empty answer also returns — so the
|
|
17
|
+
# return value cannot tell the two apart. Orchestration::DeliveryRecord
|
|
18
|
+
# needs them apart, because a turn that handed the provider nothing
|
|
19
|
+
# delivered nothing, and AiAgentJob still marks such a turn `done`.
|
|
20
|
+
#
|
|
21
|
+
# An error *after* the provider started answering is not one of these. It
|
|
22
|
+
# had the payload and acted on it; the truncated reply and the retry beside
|
|
23
|
+
# it are the ordinary ending the product already shows, and the comments
|
|
24
|
+
# that turn swallowed did reach the agent. "Started answering" is any chunk,
|
|
25
|
+
# not any *content* — see the stream block for why a tool-call chunk is the
|
|
26
|
+
# sharper case.
|
|
27
|
+
def last_handoff_failed?
|
|
28
|
+
@last_handoff_failed
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Whether the last #chat got its payload to the provider — the same
|
|
32
|
+
# boundary, asked positively, for the callers the flag above cannot serve.
|
|
33
|
+
#
|
|
34
|
+
# #last_handoff_failed? is only ever set from a rescue, so it says nothing
|
|
35
|
+
# about a #chat that left by exception past it: a user pressing Stop
|
|
36
|
+
# mid-answer raises CancelledError through here, and the turn ends in a
|
|
37
|
+
# status every reader counts as undelivered although the provider has the
|
|
38
|
+
# payload. Answering that needs the fact recorded as it happens rather than
|
|
39
|
+
# inferred from how the call ended.
|
|
40
|
+
def handed_off?
|
|
41
|
+
@handed_off
|
|
42
|
+
end
|
|
43
|
+
|
|
13
44
|
# Vendor <select> options for AI-agent config. Core ships only its built-in
|
|
14
45
|
# (stateless) providers; vendor engines append their own through
|
|
15
46
|
# register_vendor_option, so core never names a vendor engine.
|
|
16
47
|
BASE_VENDOR_OPTIONS = [
|
|
17
48
|
[ "Google (Gemini)", "google" ],
|
|
18
49
|
[ "OpenAI", "openai" ],
|
|
19
|
-
[ "Anthropic", "anthropic" ]
|
|
50
|
+
[ "Anthropic", "anthropic" ],
|
|
51
|
+
[ "CLI Proxy", "cli_proxy" ]
|
|
20
52
|
].freeze
|
|
21
53
|
|
|
22
54
|
class << self
|
|
@@ -54,10 +86,16 @@ module Collavre
|
|
|
54
86
|
end
|
|
55
87
|
|
|
56
88
|
# log_interactions: persist each call to ActivityLog. Default true. Pass false
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
|
|
89
|
+
# when a caller must not persist sensitive request or response content.
|
|
90
|
+
#
|
|
91
|
+
# before_tool_call: callable run before every tool execution, on every
|
|
92
|
+
# iteration of the request->tools->request loop. This is the only
|
|
93
|
+
# cancellation/deadline checkpoint a tool-only turn has: tool-call chunks
|
|
94
|
+
# carry no content, so #chat never yields them to the caller's streaming
|
|
95
|
+
# block, where the text path runs its check. A CancelledError raised here
|
|
96
|
+
# propagates out of #chat as a cancellation, not an "⚠️ AI Error" delta.
|
|
97
|
+
def initialize(vendor:, model:, system_prompt:, llm_api_key: nil, gateway_url: nil, context: {},
|
|
98
|
+
log_interactions: true, before_tool_call: nil, request_timeout_seconds: nil)
|
|
61
99
|
@vendor = vendor
|
|
62
100
|
@model = model
|
|
63
101
|
@system_prompt = system_prompt
|
|
@@ -65,11 +103,17 @@ module Collavre
|
|
|
65
103
|
@gateway_url = gateway_url
|
|
66
104
|
@context = context
|
|
67
105
|
@log_interactions = log_interactions
|
|
106
|
+
@before_tool_call = before_tool_call
|
|
107
|
+
@request_timeout_seconds = request_timeout_seconds
|
|
68
108
|
@last_input_tokens = 0
|
|
69
109
|
@last_output_tokens = 0
|
|
110
|
+
@last_handoff_failed = false
|
|
111
|
+
@handed_off = false
|
|
70
112
|
end
|
|
71
113
|
|
|
72
114
|
def chat(contents, tools: [], &block)
|
|
115
|
+
@last_handoff_failed = false
|
|
116
|
+
@handed_off = false
|
|
73
117
|
response_content = +""
|
|
74
118
|
error_message = nil
|
|
75
119
|
input_tokens = nil
|
|
@@ -84,6 +128,17 @@ module Collavre
|
|
|
84
128
|
add_messages(@conversation, contents)
|
|
85
129
|
|
|
86
130
|
response = @conversation.complete do |chunk|
|
|
131
|
+
# A chunk at all is the provider having accepted the request and begun
|
|
132
|
+
# answering — the earliest acceptance observable from here, and the
|
|
133
|
+
# analogue of the gateway run id the OpenClaw adapter records.
|
|
134
|
+
#
|
|
135
|
+
# Set above the empty check, not below it: role-only and tool-call
|
|
136
|
+
# chunks carry no content, and a conversation that reached its tools has
|
|
137
|
+
# not merely been handed the payload, it has acted on it — this
|
|
138
|
+
# product's tools write creatives and post comments. A turn classified
|
|
139
|
+
# as a failed handoff has everything it swallowed dispatched again, and
|
|
140
|
+
# the restored turn runs those tools a second time.
|
|
141
|
+
@handed_off = true
|
|
87
142
|
delta = extract_chunk_content(chunk).to_s
|
|
88
143
|
# Deliberately NOT `blank?`. A delta of exactly "\n\n" — the paragraph
|
|
89
144
|
# break, which providers routinely emit as a token of its own — is
|
|
@@ -100,7 +155,10 @@ module Collavre
|
|
|
100
155
|
end
|
|
101
156
|
|
|
102
157
|
if response
|
|
103
|
-
|
|
158
|
+
if response.content.present?
|
|
159
|
+
response_content = response.content.to_s
|
|
160
|
+
@handed_off = true
|
|
161
|
+
end
|
|
104
162
|
|
|
105
163
|
# Extract token usage directly from response object (RubyLLM style)
|
|
106
164
|
if response.respond_to?(:input_tokens)
|
|
@@ -119,14 +177,21 @@ module Collavre
|
|
|
119
177
|
rescue CancelledError
|
|
120
178
|
raise # Re-raise cancellation errors without catching them
|
|
121
179
|
rescue StandardError => e
|
|
180
|
+
# Nothing came back at all means nothing was handed over — see
|
|
181
|
+
# #last_handoff_failed?. Asked of @handed_off rather than of the buffer,
|
|
182
|
+
# and not `blank?`: a delta of exactly "\n\n" is content the branch above
|
|
183
|
+
# deliberately keeps, so `blank?` calls a stream that broke after a
|
|
184
|
+
# paragraph break a failed handoff, and the buffer either way says nothing
|
|
185
|
+
# about the content-less chunks. The two are the same
|
|
186
|
+
# boundary asked two ways, and Task#ended_undelivered? reads a row carrying
|
|
187
|
+
# both as undelivered — so it has to be impossible to record both.
|
|
188
|
+
@last_handoff_failed = !@handed_off
|
|
122
189
|
error_message = "[#{e.class.name}] #{e.message}"
|
|
123
|
-
# When log_interactions is false
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
# no-log guarantee already enforced on the parse path (TypoCorrector) and the
|
|
127
|
-
# ActivityLog gate below. error_message stays intact for the gated ensure log
|
|
128
|
-
# and the streamed yield (which goes back to the same user).
|
|
190
|
+
# When log_interactions is false, an LLM error message can echo request text.
|
|
191
|
+
# Log only the error class so sensitive content never leaks. error_message
|
|
192
|
+
# stays intact for the gated ensure log and streamed yield to the caller.
|
|
129
193
|
Rails.logger.error "AI Client error: #{@log_interactions ? error_message : "[#{e.class.name}]"}"
|
|
194
|
+
log_error_response(e) if @log_interactions
|
|
130
195
|
Rails.logger.error "Partial response length: #{response_content.length} chars" if response_content.present?
|
|
131
196
|
Rails.logger.debug e.backtrace.join("\n")
|
|
132
197
|
yield "\n\n⚠️ AI Error: #{error_message}" if block_given?
|
|
@@ -152,11 +217,18 @@ module Collavre
|
|
|
152
217
|
def ask(prompt)
|
|
153
218
|
return nil unless @conversation
|
|
154
219
|
|
|
220
|
+
refresh_turn_boundary!(@conversation)
|
|
155
221
|
# Disable tool calls for summary generation to avoid recursive approval
|
|
156
222
|
@conversation.with_tools(replace: true)
|
|
157
223
|
response = @conversation.ask(prompt)
|
|
224
|
+
refresh_turn_boundary!(@conversation)
|
|
158
225
|
response&.content&.strip.presence
|
|
226
|
+
rescue CancelledError
|
|
227
|
+
raise
|
|
159
228
|
rescue StandardError => e
|
|
229
|
+
# A provider timeout at the turn boundary must become the deadline that
|
|
230
|
+
# caused it, not a missing summary followed by pending_approval.
|
|
231
|
+
refresh_turn_boundary!(@conversation)
|
|
160
232
|
Rails.logger.warn("AiClient#ask failed: #{e.class} #{e.message}")
|
|
161
233
|
nil
|
|
162
234
|
end
|
|
@@ -165,8 +237,47 @@ module Collavre
|
|
|
165
237
|
|
|
166
238
|
attr_reader :vendor, :model, :system_prompt, :llm_api_key, :gateway_url, :context
|
|
167
239
|
|
|
240
|
+
# RubyLLM masks provider 400s behind a generic "Invalid request - please check
|
|
241
|
+
# your input" fallback whenever the provider's error body is not in OpenAI's
|
|
242
|
+
# {error:{message}} shape — common for OpenAI-compatible gateways (Cerebras,
|
|
243
|
+
# local Ollama, etc.), whose real reason then lives only in the raw HTTP
|
|
244
|
+
# response. RubyLLM::Error carries that response (status + body); surface it so
|
|
245
|
+
# the actual cause is one grep away instead of buried in ruby_llm.log.
|
|
246
|
+
#
|
|
247
|
+
# A provider 400 body can echo the offending request (prompt or tool arguments),
|
|
248
|
+
# so the raw body is written to the app log only under debug logging — the same
|
|
249
|
+
# level that already gates RubyLLM's own request/response body log (see the
|
|
250
|
+
# ruby_llm initializer). At INFO (production) we record status + body size only,
|
|
251
|
+
# keeping user content out of centralized app logs while still capturing that the
|
|
252
|
+
# provider rejected the request and how large its reason was; raise the log level
|
|
253
|
+
# to recover the full body on demand. The whole path is additionally gated by
|
|
254
|
+
# @log_interactions at the call site, like the error-message log above.
|
|
255
|
+
def log_error_response(error)
|
|
256
|
+
return unless error.respond_to?(:response) && (response = error.response)
|
|
257
|
+
|
|
258
|
+
status = response.respond_to?(:status) ? response.status : nil
|
|
259
|
+
body = response.respond_to?(:body) ? response.body : nil
|
|
260
|
+
return if status.nil? && body.blank?
|
|
261
|
+
|
|
262
|
+
unless Rails.logger.debug?
|
|
263
|
+
size = body.is_a?(String) ? "#{body.bytesize}B" : (body.nil? ? "none" : "non-string")
|
|
264
|
+
Rails.logger.error "AI Client error response: status=#{status} body_size=#{size} " \
|
|
265
|
+
"(body suppressed at INFO; raise log level to capture it)"
|
|
266
|
+
return
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Provider error bodies are frequently tagged ASCII-8BIT even though the bytes
|
|
270
|
+
# are valid UTF-8; reinterpret and scrub so non-ASCII text (e.g. Korean) is
|
|
271
|
+
# readable and never re-triggers the transcode failure we are eliminating.
|
|
272
|
+
body_text = body.is_a?(String) ? body.dup.force_encoding("UTF-8").scrub : body.inspect
|
|
273
|
+
Rails.logger.error "AI Client error response: status=#{status} body=#{body_text.to_s.truncate(2000)}"
|
|
274
|
+
rescue StandardError => e
|
|
275
|
+
Rails.logger.debug "AiClient#log_error_response failed: #{e.class}"
|
|
276
|
+
end
|
|
277
|
+
|
|
168
278
|
VENDOR_TO_PROVIDER = {
|
|
169
279
|
"openai" => :openai,
|
|
280
|
+
"cli_proxy" => :openai,
|
|
170
281
|
"anthropic" => :anthropic,
|
|
171
282
|
"google" => :gemini,
|
|
172
283
|
"gemini" => :gemini
|
|
@@ -176,9 +287,32 @@ module Collavre
|
|
|
176
287
|
normalized_vendor = @vendor.to_s.downcase
|
|
177
288
|
|
|
178
289
|
context_block = case normalized_vendor
|
|
179
|
-
when "openai"
|
|
180
|
-
|
|
181
|
-
|
|
290
|
+
when "openai", "cli_proxy"
|
|
291
|
+
if normalized_vendor == "cli_proxy"
|
|
292
|
+
agent = context[:user]
|
|
293
|
+
gateway = agent&.agent_gateway
|
|
294
|
+
raise ArgumentError, "CLI Proxy agent has no active gateway" unless gateway&.active?
|
|
295
|
+
|
|
296
|
+
if context.key?(:workspace_user)
|
|
297
|
+
workspace_user = context[:workspace_user]
|
|
298
|
+
else
|
|
299
|
+
comment_user = context[:comment]&.user
|
|
300
|
+
workspace_user = comment_user unless comment_user&.ai_user?
|
|
301
|
+
workspace_user ||= agent.creator
|
|
302
|
+
end
|
|
303
|
+
if gateway.per_user? && workspace_user.nil?
|
|
304
|
+
raise ArgumentError, "CLI Proxy per-user workspace requires a verified human principal"
|
|
305
|
+
end
|
|
306
|
+
workspace_user = nil if gateway.shared?
|
|
307
|
+
workspace = Collavre::AgentWorkspace.resolve!(agent: agent, user: workspace_user)
|
|
308
|
+
gateway = workspace.agent_gateway
|
|
309
|
+
@cli_proxy_identity = { gateway: gateway, workspace: workspace }
|
|
310
|
+
api_key = gateway.completion_key
|
|
311
|
+
base_url = gateway.completion_base_url
|
|
312
|
+
else
|
|
313
|
+
api_key = @llm_api_key.presence || IntegrationSettings.fetch(:openai_api_key)
|
|
314
|
+
base_url = @gateway_url.presence
|
|
315
|
+
end
|
|
182
316
|
# A custom OpenAI-compatible gateway (local Ollama / LM Studio, etc.) needs
|
|
183
317
|
# no real OpenAI key, but RubyLLM raises ConfigurationError before sending
|
|
184
318
|
# if openai_api_key is blank. Supply a placeholder so keyless local gateways
|
|
@@ -187,6 +321,9 @@ module Collavre
|
|
|
187
321
|
proc do |config|
|
|
188
322
|
config.openai_api_key = api_key
|
|
189
323
|
config.openai_api_base = base_url if base_url
|
|
324
|
+
if normalized_vendor == "cli_proxy" && !gateway.owner.system_admin? && !gateway.desktop_loopback?
|
|
325
|
+
config.faraday_adapter = Collavre::CliProxy::SafeNetHttpAdapter
|
|
326
|
+
end
|
|
190
327
|
end
|
|
191
328
|
when "anthropic"
|
|
192
329
|
api_key = @llm_api_key.presence || IntegrationSettings.fetch(:anthropic_api_key)
|
|
@@ -201,17 +338,34 @@ module Collavre
|
|
|
201
338
|
chat_opts[:provider] = provider if provider
|
|
202
339
|
chat_opts[:assume_model_exists] = true if provider
|
|
203
340
|
|
|
204
|
-
#
|
|
205
|
-
|
|
341
|
+
# RubyLLM::Context duplicates the global config. Set the timeout on that
|
|
342
|
+
# per-client copy so concurrent agent threads cannot overwrite one
|
|
343
|
+
# another's deadline cap.
|
|
344
|
+
@ruby_llm_context = RubyLLM.context do |config|
|
|
345
|
+
context_block.call(config)
|
|
346
|
+
config.request_timeout = effective_request_timeout_seconds
|
|
347
|
+
end
|
|
206
348
|
|
|
207
|
-
|
|
208
|
-
.chat(**chat_opts).tap do |chat|
|
|
349
|
+
@ruby_llm_context.chat(**chat_opts).tap do |chat|
|
|
209
350
|
chat.with_instructions(system_prompt) if system_prompt.present?
|
|
210
|
-
|
|
211
|
-
chat.with_headers("X-Session-Id" => session_id) if session_id
|
|
351
|
+
apply_request_headers!(chat)
|
|
212
352
|
chat.on_tool_call do |tool_call|
|
|
353
|
+
# Cancellation ahead of the approval gate: a turn that already
|
|
354
|
+
# reached a terminal status or its deadline must end, not park
|
|
355
|
+
# itself as pending approval for a tool it will never run. Force this
|
|
356
|
+
# boundary through the lifecycle throttle: the first tool call can
|
|
357
|
+
# arrive during the manager's initial one-second quiet period.
|
|
358
|
+
@before_tool_call&.call(true)
|
|
213
359
|
check_tool_approval!(tool_call)
|
|
214
360
|
end
|
|
361
|
+
if @request_timeout_seconds || @cli_proxy_identity
|
|
362
|
+
chat.after_tool_result do |_result|
|
|
363
|
+
# A tool can consume much of the turn. Recheck the deadline and
|
|
364
|
+
# refresh request-scoped state before the loop starts its next
|
|
365
|
+
# provider request.
|
|
366
|
+
refresh_turn_boundary!(chat)
|
|
367
|
+
end
|
|
368
|
+
end
|
|
215
369
|
if tools.any?
|
|
216
370
|
# Resolve tool names to classes using the gem's helper
|
|
217
371
|
tool_classes = ::Tools::MetaToolService.ruby_llm_tools(tools)
|
|
@@ -220,6 +374,42 @@ module Collavre
|
|
|
220
374
|
end
|
|
221
375
|
end
|
|
222
376
|
|
|
377
|
+
def refresh_turn_boundary!(conversation)
|
|
378
|
+
if @request_timeout_seconds
|
|
379
|
+
@before_tool_call&.call(true)
|
|
380
|
+
if @ruby_llm_context
|
|
381
|
+
@ruby_llm_context.config.request_timeout = effective_request_timeout_seconds
|
|
382
|
+
conversation.with_context(@ruby_llm_context)
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
apply_request_headers!(conversation)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def apply_request_headers!(conversation)
|
|
390
|
+
headers = {}
|
|
391
|
+
session_id = build_session_id
|
|
392
|
+
headers["X-Session-Id"] = session_id if session_id
|
|
393
|
+
|
|
394
|
+
if @cli_proxy_identity
|
|
395
|
+
headers.merge!(Collavre::CliProxy::Identity.headers(
|
|
396
|
+
**@cli_proxy_identity,
|
|
397
|
+
method: :post,
|
|
398
|
+
path: "/v1/chat/completions"
|
|
399
|
+
))
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
conversation.with_headers(**headers) if headers.present?
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def effective_request_timeout_seconds
|
|
406
|
+
configured = SystemSetting.llm_request_timeout_seconds.to_f
|
|
407
|
+
remaining = @request_timeout_seconds&.call
|
|
408
|
+
return configured unless remaining
|
|
409
|
+
|
|
410
|
+
[ configured, remaining.to_f ].min.clamp(0.001, configured)
|
|
411
|
+
end
|
|
412
|
+
|
|
223
413
|
def build_session_id
|
|
224
414
|
creative_id = context&.dig(:creative)&.id
|
|
225
415
|
topic_id = context&.dig(:comment)&.topic_id || context&.dig(:topic_id)
|