rubino-agent 0.3.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +115 -0
- data/.rubocop_todo.yml +955 -0
- data/.ruby-version +1 -0
- data/AGENTS.md +97 -0
- data/CHANGELOG.md +344 -0
- data/CONTRIBUTING.md +69 -0
- data/LICENSE +21 -0
- data/README.md +200 -0
- data/Rakefile +8 -0
- data/docs/agents.md +190 -0
- data/docs/api/v1.md +414 -0
- data/docs/architecture.md +177 -0
- data/docs/commands.md +375 -0
- data/docs/configuration.md +590 -0
- data/docs/getting-started.md +143 -0
- data/docs/jobs.md +332 -0
- data/docs/mcp.md +128 -0
- data/docs/memory.md +98 -0
- data/docs/models-and-keys.md +173 -0
- data/docs/oauth-providers.md +145 -0
- data/docs/plugins.md +195 -0
- data/docs/security.md +145 -0
- data/docs/skills.md +322 -0
- data/docs/tools.md +395 -0
- data/docs/troubleshooting.md +73 -0
- data/exe/rubino +9 -0
- data/install.sh +275 -0
- data/lib/rubino/active_skill.rb +50 -0
- data/lib/rubino/agent/agent_registry.rb +120 -0
- data/lib/rubino/agent/backoff_policy.rb +116 -0
- data/lib/rubino/agent/definition.rb +128 -0
- data/lib/rubino/agent/degenerate_recovery.rb +271 -0
- data/lib/rubino/agent/fallback_chain.rb +194 -0
- data/lib/rubino/agent/iteration_budget.rb +50 -0
- data/lib/rubino/agent/loop.rb +617 -0
- data/lib/rubino/agent/model_call_runner.rb +383 -0
- data/lib/rubino/agent/prompts/build.txt +69 -0
- data/lib/rubino/agent/prompts/compaction.txt +20 -0
- data/lib/rubino/agent/prompts/explore.txt +19 -0
- data/lib/rubino/agent/prompts/general.txt +20 -0
- data/lib/rubino/agent/prompts/plan.txt +31 -0
- data/lib/rubino/agent/response_validator.rb +70 -0
- data/lib/rubino/agent/router.rb +65 -0
- data/lib/rubino/agent/runner.rb +195 -0
- data/lib/rubino/agent/tool_executor.rb +402 -0
- data/lib/rubino/agent/truncation_continuation.rb +137 -0
- data/lib/rubino/api/middleware/auth.rb +43 -0
- data/lib/rubino/api/middleware/error_handler.rb +65 -0
- data/lib/rubino/api/middleware/json_parser.rb +100 -0
- data/lib/rubino/api/middleware/observability.rb +59 -0
- data/lib/rubino/api/middleware/rate_limit.rb +136 -0
- data/lib/rubino/api/operations/approvals/decide_operation.rb +49 -0
- data/lib/rubino/api/operations/clarifications/decide_operation.rb +44 -0
- data/lib/rubino/api/operations/cron_jobs/create_operation.rb +46 -0
- data/lib/rubino/api/operations/cron_jobs/delete_operation.rb +36 -0
- data/lib/rubino/api/operations/cron_jobs/list_operation.rb +55 -0
- data/lib/rubino/api/operations/cron_jobs/pause_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/resume_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/schedule_validation.rb +30 -0
- data/lib/rubino/api/operations/cron_jobs/show_operation.rb +32 -0
- data/lib/rubino/api/operations/cron_jobs/trigger_operation.rb +38 -0
- data/lib/rubino/api/operations/cron_jobs/update_operation.rb +42 -0
- data/lib/rubino/api/operations/files/read_operation.rb +40 -0
- data/lib/rubino/api/operations/files/upload_operation.rb +175 -0
- data/lib/rubino/api/operations/health_operation.rb +46 -0
- data/lib/rubino/api/operations/memory/delete_operation.rb +32 -0
- data/lib/rubino/api/operations/memory/index_operation.rb +80 -0
- data/lib/rubino/api/operations/memory/stats_operation.rb +28 -0
- data/lib/rubino/api/operations/metrics_operation.rb +18 -0
- data/lib/rubino/api/operations/mode/show_operation.rb +29 -0
- data/lib/rubino/api/operations/mode/update_operation.rb +42 -0
- data/lib/rubino/api/operations/models/list_operation.rb +45 -0
- data/lib/rubino/api/operations/oauth/connections/disconnect_operation.rb +77 -0
- data/lib/rubino/api/operations/oauth/connections/list_operation.rb +36 -0
- data/lib/rubino/api/operations/oauth/providers/callback_operation.rb +82 -0
- data/lib/rubino/api/operations/oauth/providers/connect_operation.rb +44 -0
- data/lib/rubino/api/operations/oauth/providers/list_operation.rb +35 -0
- data/lib/rubino/api/operations/oauth/serializer.rb +21 -0
- data/lib/rubino/api/operations/runs/create_operation.rb +77 -0
- data/lib/rubino/api/operations/runs/events_operation.rb +195 -0
- data/lib/rubino/api/operations/runs/stop_operation.rb +34 -0
- data/lib/rubino/api/operations/sessions/create_operation.rb +46 -0
- data/lib/rubino/api/operations/sessions/delete_operation.rb +33 -0
- data/lib/rubino/api/operations/sessions/index_operation.rb +82 -0
- data/lib/rubino/api/operations/sessions/retry_operation.rb +45 -0
- data/lib/rubino/api/operations/sessions/show_operation.rb +59 -0
- data/lib/rubino/api/operations/sessions/undo_operation.rb +38 -0
- data/lib/rubino/api/operations/skills/list_operation.rb +34 -0
- data/lib/rubino/api/operations/skills/toggle_operation.rb +40 -0
- data/lib/rubino/api/operations/tasks/index_operation.rb +30 -0
- data/lib/rubino/api/operations/tasks/serializer.rb +60 -0
- data/lib/rubino/api/operations/tasks/show_operation.rb +33 -0
- data/lib/rubino/api/operations/tasks/stop_operation.rb +47 -0
- data/lib/rubino/api/request.rb +54 -0
- data/lib/rubino/api/responses.rb +64 -0
- data/lib/rubino/api/router.rb +72 -0
- data/lib/rubino/api/schemas.rb +103 -0
- data/lib/rubino/api/server.rb +102 -0
- data/lib/rubino/api/tls.rb +108 -0
- data/lib/rubino/attachments/classification.rb +16 -0
- data/lib/rubino/attachments/classify.rb +171 -0
- data/lib/rubino/attachments/defang.rb +47 -0
- data/lib/rubino/attachments/policy.rb +36 -0
- data/lib/rubino/attachments/preamble.rb +120 -0
- data/lib/rubino/boot/encryption_key.rb +32 -0
- data/lib/rubino/cli/chat/bang_shell.rb +257 -0
- data/lib/rubino/cli/chat/completion_builder.rb +290 -0
- data/lib/rubino/cli/chat/idle_card_host.rb +69 -0
- data/lib/rubino/cli/chat/image_inbox.rb +168 -0
- data/lib/rubino/cli/chat/session_resolver.rb +176 -0
- data/lib/rubino/cli/chat_command.rb +1674 -0
- data/lib/rubino/cli/commands.rb +250 -0
- data/lib/rubino/cli/config_command.rb +96 -0
- data/lib/rubino/cli/doctor_command.rb +251 -0
- data/lib/rubino/cli/jobs_command.rb +60 -0
- data/lib/rubino/cli/memory_command.rb +135 -0
- data/lib/rubino/cli/onboarding_wizard.rb +207 -0
- data/lib/rubino/cli/server_command.rb +139 -0
- data/lib/rubino/cli/session_command.rb +125 -0
- data/lib/rubino/cli/setup_command.rb +107 -0
- data/lib/rubino/cli/skills_command.rb +85 -0
- data/lib/rubino/cli/tools_command.rb +81 -0
- data/lib/rubino/cli/trust_gate.rb +71 -0
- data/lib/rubino/commands/built_ins.rb +46 -0
- data/lib/rubino/commands/command.rb +116 -0
- data/lib/rubino/commands/executor.rb +550 -0
- data/lib/rubino/commands/handlers/agents.rb +510 -0
- data/lib/rubino/commands/handlers/config.rb +88 -0
- data/lib/rubino/commands/handlers/help.rb +148 -0
- data/lib/rubino/commands/handlers/jobs.rb +71 -0
- data/lib/rubino/commands/handlers/mcp.rb +229 -0
- data/lib/rubino/commands/handlers/memory.rb +200 -0
- data/lib/rubino/commands/handlers/sessions.rb +207 -0
- data/lib/rubino/commands/handlers/skills.rb +195 -0
- data/lib/rubino/commands/handlers/status.rb +211 -0
- data/lib/rubino/commands/loader.rb +90 -0
- data/lib/rubino/config/configuration.rb +455 -0
- data/lib/rubino/config/defaults.rb +569 -0
- data/lib/rubino/config/loader.rb +115 -0
- data/lib/rubino/config/reasoning_prefs.rb +67 -0
- data/lib/rubino/config/writer.rb +72 -0
- data/lib/rubino/context/compressor.rb +149 -0
- data/lib/rubino/context/environment_inspector.rb +176 -0
- data/lib/rubino/context/file_discovery.rb +45 -0
- data/lib/rubino/context/message_boundary.rb +39 -0
- data/lib/rubino/context/prompt_assembler.rb +382 -0
- data/lib/rubino/context/summary_builder.rb +159 -0
- data/lib/rubino/context/token_budget.rb +68 -0
- data/lib/rubino/context/tool_pair_sanitizer.rb +70 -0
- data/lib/rubino/database/connection.rb +77 -0
- data/lib/rubino/database/migrations/001_create_initial_schema.rb +156 -0
- data/lib/rubino/database/migrations/002_create_runs.rb +45 -0
- data/lib/rubino/database/migrations/003_create_skill_states.rb +15 -0
- data/lib/rubino/database/migrations/004_create_cron_jobs.rb +36 -0
- data/lib/rubino/database/migrations/005_create_oauth_connections.rb +27 -0
- data/lib/rubino/database/migrations/006_create_webhook_deliveries.rb +34 -0
- data/lib/rubino/database/migrations/007_create_messages_fts.rb +59 -0
- data/lib/rubino/database/migrations/008_create_memory_facts.rb +75 -0
- data/lib/rubino/database/migrations/009_create_memory_graph.rb +55 -0
- data/lib/rubino/database/migrations/010_add_owner_pid_to_sessions.rb +20 -0
- data/lib/rubino/database/migrator.rb +48 -0
- data/lib/rubino/documents/converters/csv.rb +79 -0
- data/lib/rubino/documents/converters/docx.rb +129 -0
- data/lib/rubino/documents/converters/html.rb +28 -0
- data/lib/rubino/documents/converters/json.rb +35 -0
- data/lib/rubino/documents/converters/pdf.rb +59 -0
- data/lib/rubino/documents/converters/plain.rb +68 -0
- data/lib/rubino/documents/converters/pptx.rb +64 -0
- data/lib/rubino/documents/converters/xlsx.rb +62 -0
- data/lib/rubino/documents/converters/xml.rb +45 -0
- data/lib/rubino/documents/html.rb +71 -0
- data/lib/rubino/documents/registry.rb +68 -0
- data/lib/rubino/documents/table.rb +63 -0
- data/lib/rubino/documents.rb +50 -0
- data/lib/rubino/errors.rb +119 -0
- data/lib/rubino/files/workspace.rb +93 -0
- data/lib/rubino/interaction/cancel_token.rb +43 -0
- data/lib/rubino/interaction/clipboard_image.rb +84 -0
- data/lib/rubino/interaction/event_bus.rb +48 -0
- data/lib/rubino/interaction/events.rb +101 -0
- data/lib/rubino/interaction/image_input.rb +127 -0
- data/lib/rubino/interaction/input_queue.rb +117 -0
- data/lib/rubino/interaction/lifecycle.rb +299 -0
- data/lib/rubino/interaction/probe.rb +65 -0
- data/lib/rubino/interaction/state.rb +56 -0
- data/lib/rubino/jobs/cron_job_repository.rb +75 -0
- data/lib/rubino/jobs/handlers/cleanup_sessions_job.rb +32 -0
- data/lib/rubino/jobs/handlers/compact_session_job.rb +21 -0
- data/lib/rubino/jobs/handlers/distill_skill_job.rb +186 -0
- data/lib/rubino/jobs/handlers/extract_memory_job.rb +37 -0
- data/lib/rubino/jobs/handlers/summarize_session_job.rb +21 -0
- data/lib/rubino/jobs/queue.rb +184 -0
- data/lib/rubino/jobs/registry.rb +45 -0
- data/lib/rubino/jobs/runner.rb +79 -0
- data/lib/rubino/jobs/scheduler.rb +138 -0
- data/lib/rubino/jobs/webhook_delivery.rb +225 -0
- data/lib/rubino/jobs/worker.rb +59 -0
- data/lib/rubino/llm/adapter_factory.rb +47 -0
- data/lib/rubino/llm/adapter_response.rb +65 -0
- data/lib/rubino/llm/auxiliary_client.rb +61 -0
- data/lib/rubino/llm/bedrock_bearer_client.rb +235 -0
- data/lib/rubino/llm/content_builder.rb +55 -0
- data/lib/rubino/llm/credential_check.rb +93 -0
- data/lib/rubino/llm/error_classifier.rb +364 -0
- data/lib/rubino/llm/fake_provider.rb +292 -0
- data/lib/rubino/llm/inline_think_filter.rb +58 -0
- data/lib/rubino/llm/model_catalog.rb +29 -0
- data/lib/rubino/llm/provider_resolver.rb +48 -0
- data/lib/rubino/llm/reasoning_manager.rb +100 -0
- data/lib/rubino/llm/request.rb +56 -0
- data/lib/rubino/llm/ruby_llm_adapter.rb +794 -0
- data/lib/rubino/llm/scenario_loader.rb +68 -0
- data/lib/rubino/llm/scenario_selector.rb +80 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron-failure.yml +29 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron.yml +36 -0
- data/lib/rubino/llm/scenarios/analysis.yml +501 -0
- data/lib/rubino/llm/scenarios/complex-analysis.yml +598 -0
- data/lib/rubino/llm/scenarios/failure.yml +65 -0
- data/lib/rubino/llm/scenarios/happy-path.yml +24 -0
- data/lib/rubino/llm/scenarios/provider-quota-completed.yml +14 -0
- data/lib/rubino/llm/scenarios/wide-table.yml +121 -0
- data/lib/rubino/llm/scenarios/with-approvals.yml +50 -0
- data/lib/rubino/llm/scenarios/with-artifacts.yml +98 -0
- data/lib/rubino/llm/scenarios/with-clarify.yml +32 -0
- data/lib/rubino/llm/scenarios/with-reasoning.yml +175 -0
- data/lib/rubino/llm/scenarios/with-uploads.yml +104 -0
- data/lib/rubino/llm/thinking_support.rb +84 -0
- data/lib/rubino/llm/tool_bridge.rb +89 -0
- data/lib/rubino/logger.rb +99 -0
- data/lib/rubino/mcp/manager.rb +180 -0
- data/lib/rubino/mcp/mcp_tool_wrapper.rb +69 -0
- data/lib/rubino/mcp.rb +57 -0
- data/lib/rubino/memory/backend.rb +104 -0
- data/lib/rubino/memory/backends/default.rb +101 -0
- data/lib/rubino/memory/backends/sqlite.rb +653 -0
- data/lib/rubino/memory/backends.rb +53 -0
- data/lib/rubino/memory/deduplicator.rb +74 -0
- data/lib/rubino/memory/extractor.rb +85 -0
- data/lib/rubino/memory/flusher.rb +31 -0
- data/lib/rubino/memory/retriever.rb +50 -0
- data/lib/rubino/memory/sqlite_extraction_prompt.rb +70 -0
- data/lib/rubino/memory/sqlite_graph.rb +154 -0
- data/lib/rubino/memory/store.rb +228 -0
- data/lib/rubino/memory/threat_scanner.rb +68 -0
- data/lib/rubino/metrics.rb +175 -0
- data/lib/rubino/modes.rb +93 -0
- data/lib/rubino/oauth/connection_repository.rb +95 -0
- data/lib/rubino/oauth/provider/github.rb +75 -0
- data/lib/rubino/oauth/provider/google.rb +59 -0
- data/lib/rubino/oauth/provider.rb +149 -0
- data/lib/rubino/oauth/registry.rb +86 -0
- data/lib/rubino/oauth/token_encryptor.rb +87 -0
- data/lib/rubino/plugins/registry.rb +75 -0
- data/lib/rubino/plugins.rb +86 -0
- data/lib/rubino/run/approval_gate.rb +243 -0
- data/lib/rubino/run/attachment_downloader.rb +166 -0
- data/lib/rubino/run/event_store.rb +74 -0
- data/lib/rubino/run/executor.rb +383 -0
- data/lib/rubino/run/gate_registry.rb +39 -0
- data/lib/rubino/run/recorder.rb +69 -0
- data/lib/rubino/run/repository.rb +118 -0
- data/lib/rubino/run/session_approval_cache.rb +118 -0
- data/lib/rubino/security/allowlist_persister.rb +55 -0
- data/lib/rubino/security/approval_policy.rb +227 -0
- data/lib/rubino/security/command_allowlist.rb +24 -0
- data/lib/rubino/security/dangerous_patterns.rb +118 -0
- data/lib/rubino/security/deny_persister.rb +73 -0
- data/lib/rubino/security/doom_loop_detector.rb +43 -0
- data/lib/rubino/security/hardline_guard.rb +105 -0
- data/lib/rubino/security/pattern_matcher.rb +62 -0
- data/lib/rubino/security/prefix_deriver.rb +124 -0
- data/lib/rubino/security/readonly_commands.rb +211 -0
- data/lib/rubino/session/exporter.rb +101 -0
- data/lib/rubino/session/message.rb +77 -0
- data/lib/rubino/session/repository.rb +295 -0
- data/lib/rubino/session/store.rb +198 -0
- data/lib/rubino/session/summary_store.rb +65 -0
- data/lib/rubino/skills/prompt_index.rb +85 -0
- data/lib/rubino/skills/registry.rb +208 -0
- data/lib/rubino/skills/skill.rb +176 -0
- data/lib/rubino/skills/skill_tool.rb +215 -0
- data/lib/rubino/skills/state_repository.rb +37 -0
- data/lib/rubino/skills/toggle.rb +26 -0
- data/lib/rubino/tools/answer_child_tool.rb +83 -0
- data/lib/rubino/tools/ask_parent_tool.rb +232 -0
- data/lib/rubino/tools/attach_file_tool.rb +120 -0
- data/lib/rubino/tools/background_tasks.rb +520 -0
- data/lib/rubino/tools/base.rb +222 -0
- data/lib/rubino/tools/custom_tool_loader.rb +119 -0
- data/lib/rubino/tools/edit_tool.rb +122 -0
- data/lib/rubino/tools/git_tool.rb +71 -0
- data/lib/rubino/tools/github_tool.rb +233 -0
- data/lib/rubino/tools/glob_tool.rb +69 -0
- data/lib/rubino/tools/grep_tool.rb +206 -0
- data/lib/rubino/tools/memory_tool.rb +184 -0
- data/lib/rubino/tools/multi_edit_tool.rb +110 -0
- data/lib/rubino/tools/patch_tool.rb +260 -0
- data/lib/rubino/tools/probe_tool.rb +175 -0
- data/lib/rubino/tools/question_tool.rb +128 -0
- data/lib/rubino/tools/read_attachment_tool.rb +180 -0
- data/lib/rubino/tools/read_tool.rb +212 -0
- data/lib/rubino/tools/read_tracker.rb +98 -0
- data/lib/rubino/tools/registry.rb +166 -0
- data/lib/rubino/tools/result.rb +113 -0
- data/lib/rubino/tools/ruby_tool.rb +0 -0
- data/lib/rubino/tools/session_search_tool.rb +103 -0
- data/lib/rubino/tools/shell_input_tool.rb +96 -0
- data/lib/rubino/tools/shell_kill_tool.rb +76 -0
- data/lib/rubino/tools/shell_output_tool.rb +72 -0
- data/lib/rubino/tools/shell_registry.rb +158 -0
- data/lib/rubino/tools/shell_tail_tool.rb +118 -0
- data/lib/rubino/tools/shell_tool.rb +330 -0
- data/lib/rubino/tools/steer_tool.rb +118 -0
- data/lib/rubino/tools/subagent_probe.rb +89 -0
- data/lib/rubino/tools/summarize_file_tool.rb +182 -0
- data/lib/rubino/tools/task_result_tool.rb +90 -0
- data/lib/rubino/tools/task_stop_tool.rb +80 -0
- data/lib/rubino/tools/task_tool.rb +622 -0
- data/lib/rubino/tools/test_tool.rb +454 -0
- data/lib/rubino/tools/todo_tool.rb +93 -0
- data/lib/rubino/tools/tool_call_repository.rb +33 -0
- data/lib/rubino/tools/vision_tool.rb +85 -0
- data/lib/rubino/tools/webfetch_tool.rb +153 -0
- data/lib/rubino/tools/websearch_tool.rb +179 -0
- data/lib/rubino/tools/write_tool.rb +61 -0
- data/lib/rubino/trust.rb +88 -0
- data/lib/rubino/ui/api.rb +296 -0
- data/lib/rubino/ui/base.rb +252 -0
- data/lib/rubino/ui/bottom_composer.rb +1599 -0
- data/lib/rubino/ui/cli.rb +1987 -0
- data/lib/rubino/ui/completion_menu.rb +321 -0
- data/lib/rubino/ui/completion_source.rb +284 -0
- data/lib/rubino/ui/escape_reader.rb +169 -0
- data/lib/rubino/ui/indented_io.rb +88 -0
- data/lib/rubino/ui/input_history.rb +108 -0
- data/lib/rubino/ui/live_region.rb +183 -0
- data/lib/rubino/ui/markdown_renderer.rb +506 -0
- data/lib/rubino/ui/notifier.rb +163 -0
- data/lib/rubino/ui/null.rb +195 -0
- data/lib/rubino/ui/paste_store.rb +176 -0
- data/lib/rubino/ui/printer_base.rb +79 -0
- data/lib/rubino/ui/probe_wait_indicator.rb +75 -0
- data/lib/rubino/ui/queued_indicators.rb +66 -0
- data/lib/rubino/ui/status_bar.rb +100 -0
- data/lib/rubino/ui/stdout_proxy.rb +161 -0
- data/lib/rubino/ui/streaming_markdown.rb +186 -0
- data/lib/rubino/ui/subagent_cards.rb +134 -0
- data/lib/rubino/ui/subagent_view.rb +255 -0
- data/lib/rubino/ui.rb +21 -0
- data/lib/rubino/update_check.rb +187 -0
- data/lib/rubino/util/duration.rb +23 -0
- data/lib/rubino/util/hyperlink.rb +105 -0
- data/lib/rubino/util/output.rb +145 -0
- data/lib/rubino/util/secrets_mask.rb +83 -0
- data/lib/rubino/version.rb +5 -0
- data/lib/rubino/workspace.rb +85 -0
- data/lib/rubino-agent.rb +5 -0
- data/lib/rubino.rb +318 -0
- data/mise.toml +2 -0
- data/rubino-agent.gemspec +103 -0
- data/skills/ruby-expert/SKILL.md +67 -0
- data/skills/ruby-expert/references/concurrency.md +357 -0
- data/skills/ruby-expert/references/datetime-and-encoding.md +363 -0
- data/skills/ruby-expert/references/errors-and-types.md +460 -0
- data/skills/ruby-expert/references/gem-authoring.md +459 -0
- data/skills/ruby-expert/references/language-idioms.md +465 -0
- data/skills/ruby-expert/references/metaprogramming.md +339 -0
- data/skills/ruby-expert/references/oo-design.md +553 -0
- data/skills/ruby-expert/references/performance.md +383 -0
- data/skills/ruby-expert/references/rails.md +424 -0
- data/skills/ruby-expert/references/security.md +404 -0
- data/skills/ruby-expert/references/testing.md +473 -0
- data/skills/ruby-expert/references/tooling.md +466 -0
- metadata +856 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Rubino
|
|
6
|
+
module API
|
|
7
|
+
module Operations
|
|
8
|
+
module Runs
|
|
9
|
+
# GET /v1/runs/:id/events — Server-Sent Events stream.
|
|
10
|
+
#
|
|
11
|
+
# Replays persisted events (honoring the `Last-Event-ID` header for
|
|
12
|
+
# resume), then polls for new ones at POLL_INTERVAL until the run
|
|
13
|
+
# reaches a terminal status (completed/failed/stopped) or disappears.
|
|
14
|
+
# Puma handles the chunked transfer transparently.
|
|
15
|
+
#
|
|
16
|
+
# @return [[Integer, Hash, Enumerable]] 200 + SSE headers + lazy streamer.
|
|
17
|
+
# @raise [Rubino::NotFoundError] when the run does not exist.
|
|
18
|
+
class EventsOperation
|
|
19
|
+
TERMINAL_STATUSES = %w[completed failed stopped].freeze
|
|
20
|
+
POLL_INTERVAL = 0.25
|
|
21
|
+
# Proxies (nginx, caddy, ALB) close idle connections around 30–60s;
|
|
22
|
+
# 15s leaves margin and also exercises the write path so we notice
|
|
23
|
+
# client disconnects (EPIPE/ECONNRESET) without waiting for a real event.
|
|
24
|
+
HEARTBEAT_INTERVAL = 15.0
|
|
25
|
+
HEARTBEAT_FRAME = ": heartbeat\n\n"
|
|
26
|
+
# Watchdog: if the run is still "running" but no new event has been
|
|
27
|
+
# written for this many seconds, give up and mark it failed. Covers
|
|
28
|
+
# cases the Executor's rescue can't (model in an infinite tool loop,
|
|
29
|
+
# provider stream silently stalled, OS thread killed by a signal we
|
|
30
|
+
# never saw). Generous enough to outlast a slow tool call but well
|
|
31
|
+
# under the SSE consumer's job timeout. Tunable via config so an op can dial
|
|
32
|
+
# it down for short tasks or up for legit long-running computations.
|
|
33
|
+
DEFAULT_IDLE_EVENT_TIMEOUT = 300.0
|
|
34
|
+
# Writes to a closed/aborted socket surface as one of these; we treat
|
|
35
|
+
# them all as "client gone" and stop polling so the thread doesn't
|
|
36
|
+
# leak until the run reaches a terminal status.
|
|
37
|
+
DISCONNECT_ERRORS = [Errno::EPIPE, Errno::ECONNRESET, IOError].freeze
|
|
38
|
+
|
|
39
|
+
def self.call(request)
|
|
40
|
+
new.call(request)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Accepts an alternate run repository and event store for tests.
|
|
44
|
+
# `clock` and `sleeper` are seams so heartbeat/disconnect specs can
|
|
45
|
+
# drive virtual time without sleeping in real wall-clock seconds.
|
|
46
|
+
# `idle_event_timeout` overrides the watchdog window (defaults from
|
|
47
|
+
# config so ops can dial without code changes; nil disables it).
|
|
48
|
+
def initialize(repository: nil, event_store: nil, clock: nil, sleeper: nil, idle_event_timeout: :default)
|
|
49
|
+
@repository = repository || ::Rubino::Run::Repository.new
|
|
50
|
+
@store = event_store || ::Rubino::Run::EventStore.new
|
|
51
|
+
@clock = clock || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
52
|
+
@sleeper = sleeper || ->(seconds) { sleep(seconds) }
|
|
53
|
+
@idle_event_timeout = idle_event_timeout == :default ? configured_idle_timeout : idle_event_timeout
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def call(request)
|
|
57
|
+
id = request.params.fetch("id")
|
|
58
|
+
run = @repository.find(id)
|
|
59
|
+
raise NotFoundError.new("run", id) unless run
|
|
60
|
+
|
|
61
|
+
after_seq = parse_last_event_id(request.header("Last-Event-ID"))
|
|
62
|
+
headers = {
|
|
63
|
+
"content-type" => "text/event-stream",
|
|
64
|
+
"cache-control" => "no-cache",
|
|
65
|
+
"x-accel-buffering" => "no"
|
|
66
|
+
}
|
|
67
|
+
[200, headers, build_stream(id, after_seq)]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def parse_last_event_id(header_value)
|
|
73
|
+
return nil if header_value.nil? || header_value.empty?
|
|
74
|
+
|
|
75
|
+
Integer(header_value, 10)
|
|
76
|
+
rescue ArgumentError
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def build_stream(run_id, after_seq)
|
|
81
|
+
store = @store
|
|
82
|
+
repo = @repository
|
|
83
|
+
clock = @clock
|
|
84
|
+
sleeper = @sleeper
|
|
85
|
+
idle_timeout = @idle_event_timeout
|
|
86
|
+
Enumerator.new do |y|
|
|
87
|
+
cursor = after_seq
|
|
88
|
+
now = clock.call
|
|
89
|
+
last_write_at = now
|
|
90
|
+
last_real_event_at = now
|
|
91
|
+
begin
|
|
92
|
+
# Replay persisted events first.
|
|
93
|
+
store.for_run(run_id, after_seq: cursor).each do |event|
|
|
94
|
+
cursor = event[:seq]
|
|
95
|
+
y << format_event(event)
|
|
96
|
+
last_write_at = clock.call
|
|
97
|
+
last_real_event_at = last_write_at
|
|
98
|
+
end
|
|
99
|
+
# Then poll for new events until terminal.
|
|
100
|
+
loop do
|
|
101
|
+
fresh = store.for_run(run_id, after_seq: cursor)
|
|
102
|
+
fresh.each do |event|
|
|
103
|
+
cursor = event[:seq]
|
|
104
|
+
y << format_event(event)
|
|
105
|
+
last_write_at = clock.call
|
|
106
|
+
last_real_event_at = last_write_at
|
|
107
|
+
end
|
|
108
|
+
run = repo.find(run_id)
|
|
109
|
+
break if run.nil? || TERMINAL_STATUSES.include?(run[:status])
|
|
110
|
+
|
|
111
|
+
# A run parked on a human approval/clarification is NOT idle —
|
|
112
|
+
# it is deliberately waiting. Suspend the watchdog while the
|
|
113
|
+
# run's gate has a pending decision and keep the clock fresh so
|
|
114
|
+
# the timer doesn't fire the instant the answer arrives.
|
|
115
|
+
last_real_event_at = clock.call if gate_pending?(run_id)
|
|
116
|
+
|
|
117
|
+
# Watchdog: if the run says "running" but the executor has
|
|
118
|
+
# gone silent for too long, escalate. Marks the row as
|
|
119
|
+
# failed (so the next /v1/runs query reflects truth) and
|
|
120
|
+
# appends a synthetic run.failed event so SSE consumers
|
|
121
|
+
# observe a proper terminal frame and can stop polling.
|
|
122
|
+
if idle_timeout && (clock.call - last_real_event_at) >= idle_timeout
|
|
123
|
+
|
|
124
|
+
handle_idle_timeout(repo, store, run_id, idle_timeout)
|
|
125
|
+
fresh = store.for_run(run_id, after_seq: cursor)
|
|
126
|
+
fresh.each do |event|
|
|
127
|
+
cursor = event[:seq]
|
|
128
|
+
y << format_event(event)
|
|
129
|
+
end
|
|
130
|
+
break
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
if clock.call - last_write_at >= HEARTBEAT_INTERVAL
|
|
134
|
+
y << HEARTBEAT_FRAME
|
|
135
|
+
last_write_at = clock.call
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
sleeper.call(POLL_INTERVAL)
|
|
139
|
+
end
|
|
140
|
+
rescue *DISCONNECT_ERRORS
|
|
141
|
+
# Client (or proxy) closed the connection. Nothing to clean up:
|
|
142
|
+
# falling out of the Enumerator block ends the stream and lets
|
|
143
|
+
# Puma reclaim the thread.
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# True when this run is currently blocked on a human approval or
|
|
150
|
+
# clarification. The gate lives in the in-process GateRegistry; a nil
|
|
151
|
+
# gate (run finished, or another worker) is simply "not pending".
|
|
152
|
+
def gate_pending?(run_id)
|
|
153
|
+
gate = ::Rubino::Run::GateRegistry.fetch(run_id)
|
|
154
|
+
gate.respond_to?(:pending?) && gate.pending?
|
|
155
|
+
rescue StandardError
|
|
156
|
+
false
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def configured_idle_timeout
|
|
160
|
+
cfg = Rubino.configuration if defined?(Rubino) && Rubino.respond_to?(:configuration)
|
|
161
|
+
value = cfg && cfg.respond_to?(:run_idle_event_timeout) ? cfg.run_idle_event_timeout : nil
|
|
162
|
+
value.nil? ? DEFAULT_IDLE_EVENT_TIMEOUT : value
|
|
163
|
+
rescue StandardError
|
|
164
|
+
DEFAULT_IDLE_EVENT_TIMEOUT
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# When the watchdog fires the run is, by definition, in an
|
|
168
|
+
# inconsistent state — the worker thread is alive long enough that
|
|
169
|
+
# systemd thinks the process is healthy but isn't emitting anymore.
|
|
170
|
+
# We update the DB row first (authoritative) then append the
|
|
171
|
+
# terminal event (best-effort; failure leaves the row consistent).
|
|
172
|
+
def handle_idle_timeout(repo, store, run_id, timeout_seconds)
|
|
173
|
+
error_message = "run idle: no new events for #{timeout_seconds.to_i}s"
|
|
174
|
+
Rubino.logger.warn(event: "run.idle_timeout", run_id: run_id, timeout_s: timeout_seconds)
|
|
175
|
+
repo.mark_failed!(run_id, error: error_message)
|
|
176
|
+
run = repo.find(run_id)
|
|
177
|
+
store.append(
|
|
178
|
+
session_id: run && run[:session_id],
|
|
179
|
+
run_id: run_id,
|
|
180
|
+
type: "run.failed",
|
|
181
|
+
payload: { error: error_message, reason: "idle_timeout" }
|
|
182
|
+
)
|
|
183
|
+
rescue StandardError => e
|
|
184
|
+
Rubino.logger.error(event: "run.idle_timeout_error", run_id: run_id, error: e.class.name,
|
|
185
|
+
message: e.message)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def format_event(event)
|
|
189
|
+
"id: #{event[:seq]}\nevent: #{event[:type]}\ndata: #{event[:payload_json]}\n\n"
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module API
|
|
5
|
+
module Operations
|
|
6
|
+
module Runs
|
|
7
|
+
# POST /v1/runs/:id/stop
|
|
8
|
+
# Cooperative stop: flags the run for cancellation; the executor checks
|
|
9
|
+
# between turns and exits cleanly. Returns 200 immediately — the run may
|
|
10
|
+
# still take a turn to wind down.
|
|
11
|
+
#
|
|
12
|
+
# @raise [Rubino::NotFoundError] when the run does not exist.
|
|
13
|
+
class StopOperation
|
|
14
|
+
def self.call(request)
|
|
15
|
+
new.call(request)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Accepts an alternate repository for tests.
|
|
19
|
+
def initialize(repository: nil)
|
|
20
|
+
@repository = repository || ::Rubino::Run::Repository.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(request)
|
|
24
|
+
id = request.params.fetch("id")
|
|
25
|
+
raise NotFoundError.new("run", id) unless @repository.find(id)
|
|
26
|
+
|
|
27
|
+
@repository.request_stop!(id)
|
|
28
|
+
[200, { id: id, status: "stop_requested" }]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module API
|
|
5
|
+
module Operations
|
|
6
|
+
module Sessions
|
|
7
|
+
# POST /v1/sessions
|
|
8
|
+
# Creates a session row (source="api") and returns its serialized form.
|
|
9
|
+
#
|
|
10
|
+
# @return [[Integer, Hash]] 201 + session payload.
|
|
11
|
+
# @raise [Rubino::ValidationError] when the body fails Schemas::CreateSession.
|
|
12
|
+
class CreateOperation
|
|
13
|
+
def self.call(request)
|
|
14
|
+
new.call(request)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Accepts an alternate repository for tests.
|
|
18
|
+
def initialize(repository: nil)
|
|
19
|
+
@repository = repository || ::Rubino::Session::Repository.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(request)
|
|
23
|
+
attrs = request.validate!(Schemas::CreateSession)
|
|
24
|
+
session = @repository.create(
|
|
25
|
+
source: "api",
|
|
26
|
+
title: attrs[:title],
|
|
27
|
+
parent_session_id: attrs[:parent_id]
|
|
28
|
+
)
|
|
29
|
+
[201, serialize(session)]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def serialize(session)
|
|
35
|
+
{
|
|
36
|
+
id: session[:id],
|
|
37
|
+
title: session[:title],
|
|
38
|
+
parent_id: session[:parent_session_id],
|
|
39
|
+
created_at: session[:created_at]
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module API
|
|
5
|
+
module Operations
|
|
6
|
+
module Sessions
|
|
7
|
+
# DELETE /v1/sessions/:id
|
|
8
|
+
# Cascade-deletes the session and its messages/runs/events.
|
|
9
|
+
#
|
|
10
|
+
# @return [[Integer, Hash]] 204 No Content.
|
|
11
|
+
# @raise [Rubino::NotFoundError] when the session does not exist.
|
|
12
|
+
class DeleteOperation
|
|
13
|
+
def self.call(request)
|
|
14
|
+
new.call(request)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Accepts an alternate repository for tests.
|
|
18
|
+
def initialize(repository: nil)
|
|
19
|
+
@repository = repository || ::Rubino::Session::Repository.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(request)
|
|
23
|
+
id = request.params.fetch("id")
|
|
24
|
+
raise NotFoundError.new("session", id) unless @repository.find(id)
|
|
25
|
+
|
|
26
|
+
@repository.destroy!(id)
|
|
27
|
+
Responses.no_content
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module API
|
|
5
|
+
module Operations
|
|
6
|
+
module Sessions
|
|
7
|
+
# GET /v1/sessions
|
|
8
|
+
# Lists recent sessions. When `?q=` is present, switches to FTS5 mode
|
|
9
|
+
# and returns the sessions whose messages match the query, ordered by
|
|
10
|
+
# the most-recent matching message. Reusing the same route keeps the
|
|
11
|
+
# client surface small — clients only need to learn one endpoint.
|
|
12
|
+
class IndexOperation
|
|
13
|
+
DEFAULT_LIMIT = 20
|
|
14
|
+
MAX_LIMIT = 100
|
|
15
|
+
|
|
16
|
+
def self.call(request)
|
|
17
|
+
new.call(request)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Accepts alternate dependencies for tests.
|
|
21
|
+
def initialize(repository: nil, message_store: nil)
|
|
22
|
+
@repository = repository || ::Rubino::Session::Repository.new
|
|
23
|
+
@message_store = message_store || ::Rubino::Session::Store.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call(request)
|
|
27
|
+
limit = clamp_limit(request.query["limit"])
|
|
28
|
+
q = request.query["q"].to_s.strip
|
|
29
|
+
|
|
30
|
+
sessions = q.empty? ? list_recent(limit) : search(q, limit)
|
|
31
|
+
[200, { sessions: sessions.map { |s| serialize(s) } }]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def list_recent(limit)
|
|
37
|
+
@repository.list(limit: limit)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Search mode: group FTS5 hits by session, ordered by the latest hit.
|
|
41
|
+
# The store gives us per-message rows; we collapse them down to one
|
|
42
|
+
# entry per session and look up the session row to keep the wire
|
|
43
|
+
# shape identical to list mode.
|
|
44
|
+
def search(q, limit)
|
|
45
|
+
hits = @message_store.search(query: q, limit: MAX_LIMIT)
|
|
46
|
+
ordered_ids = []
|
|
47
|
+
seen = {}
|
|
48
|
+
hits.each do |hit|
|
|
49
|
+
sid = hit[:session_id]
|
|
50
|
+
next if seen[sid]
|
|
51
|
+
|
|
52
|
+
seen[sid] = true
|
|
53
|
+
ordered_ids << sid
|
|
54
|
+
break if ordered_ids.size >= limit
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
ordered_ids.filter_map { |id| @repository.find(id) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def clamp_limit(raw)
|
|
61
|
+
n = raw.to_i
|
|
62
|
+
return DEFAULT_LIMIT if n <= 0
|
|
63
|
+
|
|
64
|
+
[n, MAX_LIMIT].min
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def serialize(session)
|
|
68
|
+
{
|
|
69
|
+
id: session[:id],
|
|
70
|
+
title: session[:title],
|
|
71
|
+
status: session[:status],
|
|
72
|
+
created_at: session[:created_at],
|
|
73
|
+
updated_at: session[:updated_at],
|
|
74
|
+
message_count: session[:message_count],
|
|
75
|
+
token_count: session[:token_count]
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module API
|
|
5
|
+
module Operations
|
|
6
|
+
module Sessions
|
|
7
|
+
# POST /v1/sessions/:id/retry
|
|
8
|
+
# Deletes the last user message and everything after it, then enqueues a
|
|
9
|
+
# fresh run with the same input. Returns 202 with the new run id.
|
|
10
|
+
#
|
|
11
|
+
# @return [[Integer, Hash]] 202 + { run_id, session_id, status: "running" }.
|
|
12
|
+
# @raise [Rubino::NotFoundError] when the session does not exist.
|
|
13
|
+
# @raise [Rubino::ConflictError] when the session has no user message to retry.
|
|
14
|
+
class RetryOperation
|
|
15
|
+
def self.call(request)
|
|
16
|
+
new.call(request)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Accepts alternate collaborators (session repo, message store, run repo, executor) for tests.
|
|
20
|
+
def initialize(session_repository: nil, message_store: nil, run_repository: nil, executor: nil)
|
|
21
|
+
@session_repo = session_repository || ::Rubino::Session::Repository.new
|
|
22
|
+
@message_store = message_store || ::Rubino::Session::Store.new
|
|
23
|
+
@run_repo = run_repository || ::Rubino::Run::Repository.new
|
|
24
|
+
@executor = executor || ::Rubino::Run::Executor.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call(request)
|
|
28
|
+
session_id = request.params.fetch("id")
|
|
29
|
+
raise NotFoundError.new("session", session_id) unless @session_repo.find(session_id)
|
|
30
|
+
|
|
31
|
+
last_user = @message_store.last_for_role(session_id, "user")
|
|
32
|
+
raise ConflictError, "no user message to retry" unless last_user
|
|
33
|
+
|
|
34
|
+
@message_store.delete_from_inclusive(session_id, from_id: last_user.id)
|
|
35
|
+
|
|
36
|
+
run = @run_repo.create(session_id: session_id, input_text: last_user.content)
|
|
37
|
+
@executor.start(run)
|
|
38
|
+
|
|
39
|
+
[202, { run_id: run[:id], session_id: session_id, status: "running" }]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Rubino
|
|
6
|
+
module API
|
|
7
|
+
module Operations
|
|
8
|
+
module Sessions
|
|
9
|
+
# GET /v1/sessions/:id
|
|
10
|
+
# Returns the session with its message timeline inlined.
|
|
11
|
+
#
|
|
12
|
+
# @raise [Rubino::NotFoundError] when the session does not exist.
|
|
13
|
+
class ShowOperation
|
|
14
|
+
def self.call(request)
|
|
15
|
+
new.call(request)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Accepts an alternate repository and message store for tests.
|
|
19
|
+
def initialize(repository: nil, message_store: nil)
|
|
20
|
+
@repository = repository || ::Rubino::Session::Repository.new
|
|
21
|
+
@message_store = message_store || ::Rubino::Session::Store.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(request)
|
|
25
|
+
id = request.params.fetch("id")
|
|
26
|
+
session = @repository.find(id)
|
|
27
|
+
raise NotFoundError.new("session", id) unless session
|
|
28
|
+
|
|
29
|
+
[200, serialize(session)]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def serialize(session)
|
|
35
|
+
{
|
|
36
|
+
id: session[:id],
|
|
37
|
+
title: session[:title],
|
|
38
|
+
instructions: nil,
|
|
39
|
+
created_at: session[:created_at],
|
|
40
|
+
status: session[:status],
|
|
41
|
+
messages: messages_for(session[:id])
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def messages_for(session_id)
|
|
46
|
+
@message_store.for_session(session_id).map do |m|
|
|
47
|
+
{
|
|
48
|
+
id: m.id,
|
|
49
|
+
role: m.role,
|
|
50
|
+
content: m.content,
|
|
51
|
+
created_at: m.created_at
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module API
|
|
5
|
+
module Operations
|
|
6
|
+
module Sessions
|
|
7
|
+
# POST /v1/sessions/:id/undo
|
|
8
|
+
# Removes the last user message and everything after it (no re-run).
|
|
9
|
+
# Returns the number of messages deleted.
|
|
10
|
+
#
|
|
11
|
+
# @raise [Rubino::NotFoundError] when the session does not exist.
|
|
12
|
+
# @raise [Rubino::ConflictError] when the session has no user message to undo.
|
|
13
|
+
class UndoOperation
|
|
14
|
+
def self.call(request)
|
|
15
|
+
new.call(request)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Accepts an alternate session repository and message store for tests.
|
|
19
|
+
def initialize(session_repository: nil, message_store: nil)
|
|
20
|
+
@session_repo = session_repository || ::Rubino::Session::Repository.new
|
|
21
|
+
@message_store = message_store || ::Rubino::Session::Store.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(request)
|
|
25
|
+
session_id = request.params.fetch("id")
|
|
26
|
+
raise NotFoundError.new("session", session_id) unless @session_repo.find(session_id)
|
|
27
|
+
|
|
28
|
+
last_user = @message_store.last_for_role(session_id, "user")
|
|
29
|
+
raise ConflictError, "nothing to undo" unless last_user
|
|
30
|
+
|
|
31
|
+
removed = @message_store.delete_from_inclusive(session_id, from_id: last_user.id)
|
|
32
|
+
[200, { removed_messages: removed }]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module API
|
|
5
|
+
module Operations
|
|
6
|
+
module Skills
|
|
7
|
+
# GET /v1/skills
|
|
8
|
+
# Lists every registered skill annotated with its persisted enabled flag.
|
|
9
|
+
class ListOperation
|
|
10
|
+
def self.call(request)
|
|
11
|
+
new.call(request)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Accepts an alternate skills registry and state repository for tests.
|
|
15
|
+
def initialize(registry: nil, state_repository: nil)
|
|
16
|
+
@registry = registry || ::Rubino::Skills::Registry.new
|
|
17
|
+
@state_repository = state_repository || ::Rubino::Skills::StateRepository.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(_request)
|
|
21
|
+
skills = @registry.all.map do |skill|
|
|
22
|
+
{
|
|
23
|
+
name: skill.name,
|
|
24
|
+
description: skill.description,
|
|
25
|
+
enabled: @state_repository.enabled?(skill.name)
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
[200, skills]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module API
|
|
5
|
+
module Operations
|
|
6
|
+
module Skills
|
|
7
|
+
# PUT /v1/skills/:name
|
|
8
|
+
# Persists the enable/disable flag for a single registered skill.
|
|
9
|
+
#
|
|
10
|
+
# @raise [Rubino::NotFoundError] when no skill is registered under +name+.
|
|
11
|
+
# @raise [Rubino::ValidationError] when the body fails Schemas::ToggleSkill.
|
|
12
|
+
class ToggleOperation
|
|
13
|
+
def self.call(request)
|
|
14
|
+
new.call(request)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Accepts an alternate skills registry and state repository for tests.
|
|
18
|
+
def initialize(registry: nil, state_repository: nil)
|
|
19
|
+
@registry = registry || ::Rubino::Skills::Registry.new
|
|
20
|
+
@state_repository = state_repository || ::Rubino::Skills::StateRepository.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(request)
|
|
24
|
+
name = request.params.fetch("name")
|
|
25
|
+
raise NotFoundError.new("skill", name) unless @registry.find(name)
|
|
26
|
+
|
|
27
|
+
attrs = request.validate!(Schemas::ToggleSkill)
|
|
28
|
+
# The shared toggle write path (Skills::Toggle, #188) — the same
|
|
29
|
+
# registry-validated StateRepository write the in-chat
|
|
30
|
+
# `/skills enable|disable` and the `rubino skills` CLI verbs run.
|
|
31
|
+
::Rubino::Skills::Toggle.set(name, enabled: attrs[:enabled],
|
|
32
|
+
registry: @registry,
|
|
33
|
+
state_repository: @state_repository)
|
|
34
|
+
[200, { name: name, enabled: attrs[:enabled] }]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module API
|
|
5
|
+
module Operations
|
|
6
|
+
module Tasks
|
|
7
|
+
# GET /v1/tasks
|
|
8
|
+
# Lists background subagents started by the `task` tool, newest first.
|
|
9
|
+
# Each row is the summary shape (no full result body — see the show
|
|
10
|
+
# endpoint for that). The registry is process-local and not persisted,
|
|
11
|
+
# so this reflects only the current server process's children.
|
|
12
|
+
class IndexOperation
|
|
13
|
+
def self.call(request)
|
|
14
|
+
new.call(request)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Accepts an alternate registry for tests.
|
|
18
|
+
def initialize(registry: nil)
|
|
19
|
+
@registry = registry || ::Rubino::Tools::BackgroundTasks.instance
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(_request)
|
|
23
|
+
tasks = @registry.list.map { |entry| Serializer.summary(entry) }
|
|
24
|
+
[200, { tasks: tasks }]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|