solid_loop 0.0.4

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.
Files changed (137) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +156 -0
  3. data/CODE_OF_CONDUCT.md +67 -0
  4. data/CONTRIBUTING.md +82 -0
  5. data/MIT-LICENSE +21 -0
  6. data/README.md +483 -0
  7. data/Rakefile +11 -0
  8. data/app/assets/javascripts/solid_loop/chart.umd.min.js +14 -0
  9. data/app/assets/javascripts/solid_loop/chartjs-adapter-date-fns.bundle.min.js +7 -0
  10. data/app/assets/javascripts/solid_loop/chartkick.min.js +2 -0
  11. data/app/assets/stylesheets/solid_loop/application.css +15 -0
  12. data/app/controllers/solid_loop/application_controller.rb +29 -0
  13. data/app/controllers/solid_loop/dashboard_controller.rb +104 -0
  14. data/app/controllers/solid_loop/events_controller.rb +12 -0
  15. data/app/controllers/solid_loop/loops_controller.rb +77 -0
  16. data/app/controllers/solid_loop/mcp_inbound_sessions_controller.rb +13 -0
  17. data/app/controllers/solid_loop/mcp_sessions_controller.rb +68 -0
  18. data/app/controllers/solid_loop/mcp_tools_controller.rb +16 -0
  19. data/app/controllers/solid_loop/messages_controller.rb +12 -0
  20. data/app/controllers/solid_loop/tool_calls_controller.rb +12 -0
  21. data/app/helpers/solid_loop/application_helper.rb +125 -0
  22. data/app/jobs/solid_loop/application_job.rb +13 -0
  23. data/app/jobs/solid_loop/llm_completion_job.rb +139 -0
  24. data/app/jobs/solid_loop/observe_broadcast_job.rb +15 -0
  25. data/app/jobs/solid_loop/reaper_job.rb +20 -0
  26. data/app/jobs/solid_loop/tool_execution_job.rb +200 -0
  27. data/app/models/solid_loop/application_record.rb +5 -0
  28. data/app/models/solid_loop/base.rb +190 -0
  29. data/app/models/solid_loop/event.rb +7 -0
  30. data/app/models/solid_loop/loop.rb +225 -0
  31. data/app/models/solid_loop/mcp_inbound_session.rb +25 -0
  32. data/app/models/solid_loop/mcp_session.rb +12 -0
  33. data/app/models/solid_loop/mcp_tool.rb +26 -0
  34. data/app/models/solid_loop/message.rb +69 -0
  35. data/app/models/solid_loop/tool_call.rb +42 -0
  36. data/app/queries/solid_loop/admin/base_query.rb +23 -0
  37. data/app/queries/solid_loop/admin/events_query.rb +31 -0
  38. data/app/queries/solid_loop/admin/loops_query.rb +38 -0
  39. data/app/queries/solid_loop/admin/mcp_inbound_sessions_query.rb +31 -0
  40. data/app/queries/solid_loop/admin/mcp_sessions_query.rb +26 -0
  41. data/app/queries/solid_loop/admin/mcp_tools_query.rb +26 -0
  42. data/app/queries/solid_loop/admin/messages_query.rb +37 -0
  43. data/app/queries/solid_loop/admin/tool_calls_query.rb +34 -0
  44. data/app/services/solid_loop/adapters/native.rb +170 -0
  45. data/app/services/solid_loop/dialects/anthropic.rb +160 -0
  46. data/app/services/solid_loop/dialects/gemini.rb +122 -0
  47. data/app/services/solid_loop/dialects/open_ai.rb +62 -0
  48. data/app/services/solid_loop/dialects/reasoning_packer.rb +37 -0
  49. data/app/services/solid_loop/llm_usage_parser/anthropic.rb +22 -0
  50. data/app/services/solid_loop/llm_usage_parser/gemini.rb +18 -0
  51. data/app/services/solid_loop/llm_usage_parser/llama.rb +15 -0
  52. data/app/services/solid_loop/llm_usage_parser/openai.rb +18 -0
  53. data/app/services/solid_loop/llm_usage_parser.rb +19 -0
  54. data/app/services/solid_loop/mcp_session_initializer.rb +205 -0
  55. data/app/services/solid_loop/mcp_tool_execution_service.rb +153 -0
  56. data/app/services/solid_loop/middlewares/agent_initialization.rb +37 -0
  57. data/app/services/solid_loop/middlewares/error_handling.rb +107 -0
  58. data/app/services/solid_loop/middlewares/event_logging.rb +100 -0
  59. data/app/services/solid_loop/middlewares/message_building.rb +92 -0
  60. data/app/services/solid_loop/middlewares/network_calling.rb +84 -0
  61. data/app/services/solid_loop/middlewares/response_parsing.rb +348 -0
  62. data/app/services/solid_loop/middlewares/tool_call_xml_parser.rb +117 -0
  63. data/app/services/solid_loop/sse_stream_aggregator.rb +105 -0
  64. data/app/services/solid_loop/tool_failure_reconciler.rb +122 -0
  65. data/app/services/solid_loop/tool_middlewares/agent_initialization.rb +17 -0
  66. data/app/services/solid_loop/tool_middlewares/error_handling.rb +66 -0
  67. data/app/services/solid_loop/tool_middlewares/event_logging.rb +27 -0
  68. data/app/services/solid_loop/tool_middlewares/response_creation.rb +110 -0
  69. data/app/services/solid_loop/tool_middlewares/tool_execution.rb +206 -0
  70. data/app/views/layouts/solid_loop/admin.html.erb +416 -0
  71. data/app/views/layouts/solid_loop/application.html.erb +17 -0
  72. data/app/views/solid_loop/dashboard/index.html.erb +184 -0
  73. data/app/views/solid_loop/events/index.html.erb +47 -0
  74. data/app/views/solid_loop/events/show.html.erb +76 -0
  75. data/app/views/solid_loop/loops/index.html.erb +83 -0
  76. data/app/views/solid_loop/loops/show.html.erb +148 -0
  77. data/app/views/solid_loop/mcp_inbound_sessions/index.html.erb +53 -0
  78. data/app/views/solid_loop/mcp_inbound_sessions/show.html.erb +78 -0
  79. data/app/views/solid_loop/mcp_sessions/_tool_output.html.erb +11 -0
  80. data/app/views/solid_loop/mcp_sessions/index.html.erb +46 -0
  81. data/app/views/solid_loop/mcp_sessions/inspector.html.erb +94 -0
  82. data/app/views/solid_loop/mcp_sessions/show.html.erb +142 -0
  83. data/app/views/solid_loop/mcp_tools/index.html.erb +44 -0
  84. data/app/views/solid_loop/mcp_tools/show.html.erb +69 -0
  85. data/app/views/solid_loop/messages/_message.html.erb +75 -0
  86. data/app/views/solid_loop/messages/index.html.erb +80 -0
  87. data/app/views/solid_loop/messages/show.html.erb +121 -0
  88. data/app/views/solid_loop/shared/_pagination.html.erb +21 -0
  89. data/app/views/solid_loop/tool_calls/index.html.erb +53 -0
  90. data/app/views/solid_loop/tool_calls/show.html.erb +59 -0
  91. data/config/routes.rb +27 -0
  92. data/db/migrate/20260408000100_solid_loop_init.rb +136 -0
  93. data/db/migrate/20260709000100_solid_loop_create_mcp_inbound_sessions.rb +16 -0
  94. data/db/migrate/20260713000100_solid_loop_add_execution_guards.rb +6 -0
  95. data/db/migrate/20260714000100_solid_loop_add_message_steering.rb +17 -0
  96. data/db/migrate/20260714000200_solid_loop_add_message_conversation_order.rb +14 -0
  97. data/db/migrate/20260715000100_solid_loop_add_mcp_inbound_session_principal_key.rb +43 -0
  98. data/db/migrate/20260715000200_solid_loop_add_llm_lease.rb +40 -0
  99. data/db/migrate/20260715000300_solid_loop_add_tool_lease.rb +33 -0
  100. data/db/migrate/20260715000400_solid_loop_add_lease_running_check.rb +32 -0
  101. data/db/migrate/20260715000500_solid_loop_add_tool_lease_pair_check.rb +35 -0
  102. data/docs/contributing/coverage.md +64 -0
  103. data/docs/decisions/durable_attempt_lease.md +364 -0
  104. data/docs/decisions/mcp-only-tooling.md +135 -0
  105. data/docs/decisions/mcp-server.md +223 -0
  106. data/docs/decisions/reasoning_persistence.md +51 -0
  107. data/docs/decisions/ruby_llm_rejected.md +35 -0
  108. data/docs/guides/dialects.md +55 -0
  109. data/docs/guides/llm_middlewares.md +294 -0
  110. data/docs/guides/mcp_transports.md +374 -0
  111. data/docs/guides/tool_middlewares.md +148 -0
  112. data/lib/solid_loop/configuration.rb +175 -0
  113. data/lib/solid_loop/engine.rb +14 -0
  114. data/lib/solid_loop/lease_heartbeat.rb +94 -0
  115. data/lib/solid_loop/lease_renewer.rb +347 -0
  116. data/lib/solid_loop/llm_metrics.rb +16 -0
  117. data/lib/solid_loop/mcp/call_context.rb +19 -0
  118. data/lib/solid_loop/mcp/client_factory.rb +61 -0
  119. data/lib/solid_loop/mcp/http_transport.rb +52 -0
  120. data/lib/solid_loop/mcp/principal.rb +82 -0
  121. data/lib/solid_loop/mcp/result.rb +13 -0
  122. data/lib/solid_loop/mcp/server.rb +246 -0
  123. data/lib/solid_loop/mcp/stdio_transport.rb +224 -0
  124. data/lib/solid_loop/mcp/toolset.rb +347 -0
  125. data/lib/solid_loop/mcp/transport.rb +20 -0
  126. data/lib/solid_loop/mcp.rb +25 -0
  127. data/lib/solid_loop/mcp_client.rb +176 -0
  128. data/lib/solid_loop/pipeline/builder.rb +38 -0
  129. data/lib/solid_loop/pipeline/context.rb +61 -0
  130. data/lib/solid_loop/pipeline/tool_context.rb +53 -0
  131. data/lib/solid_loop/pipeline.rb +40 -0
  132. data/lib/solid_loop/reaper.rb +313 -0
  133. data/lib/solid_loop/version.rb +3 -0
  134. data/lib/solid_loop.rb +94 -0
  135. data/lib/tasks/coverage.rake +206 -0
  136. data/lib/tasks/solid_loop_tasks.rake +4 -0
  137. metadata +228 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e3bbfed94f57749cc453fa556177eb4aa339e304c41d5c6e2dd58f5f55cd7c55
4
+ data.tar.gz: beda9759a9b2d1d576b007cc6782139b06c97922b7ce0bb0117685190d437f93
5
+ SHA512:
6
+ metadata.gz: b9447a7363162eac0dcc9c23964855897a427a09a50c7af701e07cbea1edd65967f72766b9e6d419a8312189d35492bf67c1a0253815c779b53e73aadb6e28a0
7
+ data.tar.gz: 3477debd8e6e9fa6b8b047fb4e7b0cb2cd36aa53edeedfa37c99e05ab1c213a0fc2618f92a22b7edb3e0a014f8d49b48dbf4400728ba4bd4f73819bc048b8a59
data/CHANGELOG.md ADDED
@@ -0,0 +1,156 @@
1
+ # SolidLoop Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.0.4] - 2026-07-16
9
+
10
+ #### Fixed
11
+ - Admin dashboard time-series charts no longer fail with "Error Loading Chart:
12
+ This method is not implemented: Check that a complete date adapter is
13
+ provided." Chart.js v4 requires a registered date adapter for the time scale;
14
+ vendor `chartjs-adapter-date-fns` (bundled with date-fns) and load it after
15
+ Chart.js and before Chartkick in the admin layout.
16
+ - Capture the failing LLM response in the wire-log `Event` on any non-2xx (e.g. OpenRouter `403`) — status, redacted response headers and a bounded (redacted) response body — and include that body in `loop.error_message` instead of the empty `"HTTP 403: "`; the streaming path now recovers the body the adapter drains via `on_data`, and connection/timeout failures are recorded too.
17
+ - Redact `Authorization`, `x-api-key`, `api-key`, and API-key query parameters from persisted LLM event URLs, wire logs, and exception messages.
18
+ - Treat MCP HTTP `404` responses as session loss so `session_recovery: :auto` can re-initialize mounted SolidLoop MCP sessions.
19
+ - Preserve Gemini signed reasoning through the complete reasoning-packer and request-rendering path.
20
+ - Resume unresolved tool steps before starting another LLM turn, with generation-token fencing so stale workers cannot overwrite pause/resume decisions.
21
+ - Checkpoint each completed tool invocation (`executed_at`) so a duplicate or retried job replays its stored result instead of repeating the tool — in both parallel and sequential execution modes.
22
+ - Preserve MCP `isError` as `ToolCall#is_success = false` while still returning the tool result to the model.
23
+ - **`ToolExecutionJob` now requires its generation token.** A job with no token, or one that does not match the loop's current `execution_token`, fails closed (no-op) instead of adopting the loop's token; the same applies to the mid-pipeline `loop_active?` checks. Legacy one-argument `ToolExecutionJob(tool_call_id)` jobs enqueued before this release will no-op — drain or `resume!` affected loops after deploying.
24
+ - **Sequential ordering is a checked invariant, not a convention.** Under `sequential_tool_calls?`, the first-unresolved guard runs under the parent assistant-message lock (the same lock results land under), and an out-of-order/stray/duplicate job no-ops **and re-dispatches the actual first unresolved call**, so a stray job self-heals the order instead of stalling the turn. Every "first/next tool call" selection goes through the explicit `ordered` scope.
25
+ - **All generation-owned writes are token-fenced.** Assistant/tool turn persistence, `triggering_message` state, loop counters and MCP session/tool sync now re-validate the execution token under the loop row lock (or a conditional re-check around network calls) before writing; a stale worker that lost a pause/resume race performs zero mutations instead of corrupting the current run.
26
+ - **Recovery only resumes the conversation tail.** `Loop#unresolved_tool_calls` starts from the newest visible user/assistant message and proceeds only when that tail is an assistant tool turn — an obsolete tool turn behind newer messages can no longer be resurrected.
27
+ - **State advancement and successor-job dispatch are atomic.** Every dispatch site (`ResponseParsing`, `ToolCallXmlParser`, `ResponseCreation`, `Base.start!`/`#resume!`) enqueues the next job *inside* the transaction that commits the state change; `SolidLoop::ApplicationJob` sets `enqueue_after_transaction_commit = false` so the enqueue participates in it. This requires a same-database transactional job backend (Solid Queue / GoodJob) — see the README's "Job backend requirement" section.
28
+
29
+ #### Added
30
+ - **Steering — enqueue a user message onto a live loop, delivered at the next
31
+ turn boundary.** `Loop#enqueue_user_message(content)` stages a `role: user`
32
+ message (`steering: true, is_hidden: true, status: pending`, new `steering`
33
+ column + partial index) that is excluded from the in-flight turn and surfaced
34
+ as still-pending; it is materialized (un-hidden, `status: success`) at the
35
+ START of the next turn by `MessageBuilding#build_messages` via
36
+ `Loop#drain_pending_steering!`. Because materialization runs at a resolved
37
+ boundary and positions by `id`, the message always appends AFTER the last
38
+ resolved exchange and never between an `assistant(tool_calls)` message and its
39
+ tool results — the chat-format pairing invariant holds without special-casing.
40
+ Enqueue is rejected on a terminal (`completed`/`failed`) loop; a `paused`
41
+ (HITL) loop keeps the message pending until the next post-resume turn.
42
+ - **Steering messages are editable/cancellable while pending.**
43
+ `Loop#edit_pending_message(id, content)` and `Loop#cancel_pending_message(id)`
44
+ mutate/void a still-staged message; `Loop#pending_user_messages` lists them in
45
+ enqueue order. Delivery is **exactly-once and crash-safe**: materialization
46
+ flips `pending -> success` under the message row lock guarded by a `pending?`
47
+ re-check, so a retried (or concurrent) turn build never re-delivers, and a
48
+ concurrent edit/cancel either wins the lock (applies cleanly, delivered next
49
+ turn) or loses it (sees the delivered status and is refused with
50
+ `SolidLoop::SteeringError`) — never a half-applied state.
51
+ - **`SolidLoop::Mcp.server(toolset, auth:)` — serving toolsets over Streamable HTTP** (see `docs/decisions/mcp-server.md`): a mountable Rack endpoint per toolset (`mount SolidLoop::Mcp.server(SearchTools, auth: McpAuth.new) => "/mcp/search"`). The POST body dispatches straight into `Toolset#deliver` — the same method the in-process agent path calls — so one toolset is consumed locally and served externally with shared schemas, wire logs and admin. Exposure is the mount itself: an unmounted toolset is not exposed.
52
+ - **Fail-closed auth with a principal:** `auth:` is required at construction and is any object responding to `call(token, request) -> principal | nil`; `nil` → empty 401. No token storage in the gem — bearer lists, DB lookups and IP allowlists live in the host's authorizer.
53
+ - **`CallContext#principal`:** external calls arrive in tool handlers as `CallContext(agent: nil, loop: nil, principal: <authorizer's return>)`. Handlers must no longer assume `ctx.agent` / `ctx.loop` are present.
54
+ - **`McpInboundSession`** (new migration): one row per external client connection — server name, principal label, `last_used_at`, `terminated_at`. `initialize` issues `Mcp-Session-Id` (honoring `Toolset#on_initialize` minting); requests without the header get `400`, unknown/terminated sessions `404`; `DELETE` terminates the session but keeps the row (a client must not be able to erase its own audit trail). Admin: **Inbound MCP** index/show pages.
55
+ - **Inbound observability:** every request that reaches a session writes a loop-less `SolidLoop::Event` (`mcp_inbound_*`, eventable = the inbound session) with the JSON-RPC envelopes and a wire log.
56
+ - **Protocol scope (deliberately narrow):** notifications (no `id`) → `202`; batch bodies → `-32600`; malformed JSON → `-32700`; **no SSE** — `GET` → `405` (the Streamable-HTTP no-stream option), POST always answers a single `application/json` response.
57
+ - **Prompts DSL on `Mcp::Toolset`:** `prompt "name", description:, arguments: [...] do |args, ctx| ... end` served via `prompts/list` / `prompts/get` on every transport (in-process and mounted); the `prompts` capability is advertised only when prompts are declared. Handler exceptions become `-32603` (prompts have no `isError` shape).
58
+ - **MCP client breadth:** `McpClient#list_prompts` / `#get_prompt` / `#list_resources` / `#read_resource` / `#server_capabilities`. Session initialization snapshots the server's capabilities — and, when advertised, the discovered prompts/resources lists — into `McpSession#metadata` (best-effort, with `mcp_list_prompts` / `mcp_list_resources` events); the admin session page shows them.
59
+ - **HITL middleware seam:** `ToolContext#skip_execution!` lets policy middleware emit a rejected/expired tool result through the standard response and continuation path without invoking the tool.
60
+ - **In-process principals:** `Base#mcp_principal` defaults to the loop subject and is passed to local toolsets through `CallContext#principal`.
61
+ - **Per-tool-call idempotency key:** every tool invocation carries `ToolCall#idempotency_key` (`solid_loop:tool_call:<id>`) — exposed to in-process handlers as `CallContext#idempotency_key` and sent to remote MCP servers as `_meta["solidloop/idempotencyKey"]` on the `tools/call` params. `executed_at` cannot cover a crash between a remote side effect and the checkpoint commit; irreversible tools should dedupe on this key (see `docs/guides/mcp_transports.md`).
62
+ - **`SolidLoop::Base#sequential_tool_calls?`** (default `false`): opt a specific agent into strict, one-at-a-time tool execution in model order (`def sequential_tool_calls? = true`). The single flag gates emission, the ordering guard and continuation together. Recommended for agents that gate tools on human approval — see _Changed_ below and `docs/guides/tool_middlewares.md`.
63
+
64
+ #### Changed
65
+ - `SolidLoop::Event#loop` is now optional (`loop_id` nullable was already in the schema): inbound MCP events have no loop.
66
+ - **Same-turn tool calls now run in parallel by default.** When an assistant turn returns N tool calls, all N `ToolExecutionJob`s are enqueued at once and run concurrently across workers; the earlier strict model-order execution is now opt-in per agent via `sequential_tool_calls?`. Durability is preserved in both modes — execution-token fencing rejects stale-turn jobs and the `executed_at` checkpoint prevents duplicate execution — and the completion gate still queues the next LLM turn only after every result exists (fenced so exactly one job queues it). Per-tool HITL approval is cleanest under sequential mode, where a single call is in flight at a time. **Rollout:** a loop caught mid-turn across the upgrade from an earlier sequential-only build may have only its first call enqueued; a parallel-default worker will not chain the remaining calls, so it can stall with results missing. Drain in-flight tool loops before deploying, or `pause!`/`resume!` an affected loop afterward — `resume!` re-dispatches *every* unresolved call in parallel mode (only the first in sequential mode).
67
+
68
+ ## [0.0.3] - 2026-07-09
69
+
70
+ #### Removed
71
+ - **Native Ruby tools — every tool is now an MCP tool** (see `docs/decisions/mcp-only-tooling.md`):
72
+ - `SolidLoop::Base#tools` and the `FUNCTION_NAME` / `to_tool_spec` conventions for tool classes.
73
+ - The Ruby-tool resolution branch in `ToolMiddlewares::ToolExecution` — one execution path remains: `loop.mcp_tools`.
74
+ - The native merge in `Middlewares::MessageBuilding`; the LLM payload advertises `loop.mcp_tools` only.
75
+ - The unknown-tool feedback message lists the loop's MCP tools only.
76
+ - `McpClient#last_response` (Faraday response) — replaced by `McpClient#last_result` (`Mcp::Result`).
77
+
78
+ #### Migration notes (0.0.2 → 0.0.3)
79
+ - Move each native tool class into a `SolidLoop::Mcp::Toolset` and replace `def tools` with an `mcps` entry — full before/after in `docs/decisions/mcp-only-tooling.md`:
80
+ ```ruby
81
+ class TaskTools < SolidLoop::Mcp::Toolset
82
+ server_name "tasks"
83
+ tool "check_task", description: "...", input_schema: { type: "object" } do |args, ctx|
84
+ "Task verified: #{args['path']}"
85
+ end
86
+ end
87
+
88
+ class MyAgent < SolidLoop::Base
89
+ def mcps = [ { name: :tasks, toolset: TaskTools } ]
90
+ end
91
+ ```
92
+ - Behavior change: exceptions raised inside a tool no longer fail the loop — they return an `isError` tool result the model can self-correct on (standard MCP semantics).
93
+ - Toolset schemas live in code, so discovery is free — agents using toolsets should return `true` from `refresh_mcp_tools?` so schemas persisted in `mcp_tools` don't go stale across deploys.
94
+
95
+ #### Added
96
+ - **MCP Transport Port:** `SolidLoop::Mcp::Transport` (a `deliver(payload, session_id:, context:)` port) and `SolidLoop::Mcp::Result` (body, session_id, raw_request, raw_response, status), so MCP servers can be reached over pluggable transports.
97
+ - **`Mcp::HttpTransport`:** the Faraday implementation extracted from `McpClient` as the reference adapter; connection-level failures (timeouts, refused connections) are now wrapped in `SolidLoop::McpError`.
98
+ - **Transport conformance suite:** RSpec shared examples (`"a solid_loop mcp transport"`) covering session identity, raw request/response presence, protocol vs transport error semantics, and thread safety of concurrent `deliver` calls.
99
+ - **`Mcp::Toolset` (in-process MCP server):** the home of former native tools. `server_name` / `tool` DSL (name, description, input_schema, block); `deliver` dispatches `initialize` / `tools/list` / `tools/call` in-process with the JSON boundary preserved (blocks receive string-keyed arguments exactly as over HTTP). String results become text content; Hash results are serialized to JSON text content **and** `structuredContent`; raised exceptions become `isError: true` tool results. Stateless by default; `on_initialize` override mints real session ids.
100
+ - **`Mcp::CallContext` (`agent:`, `loop:`):** threaded from `McpSessionInitializer` / `McpToolExecutionService` through the client into every `Transport#deliver`, so in-process toolsets reach domain state without a network hop.
101
+ - **`mcps` config entry forms:** `toolset:` (class or instance) and `transport:` (any `Mcp::Transport` object) alongside the existing `url:` sugar; resolved by `Mcp::ClientFactory`. Non-HTTP transports default to `stateless: true`. `required_tools:` is now accepted as the explicit validation list (falls back to `tools:` as before).
102
+ - **Tool-name prefixes and collision validation:** `prefix: "search"` advertises tools to the LLM as `search__fetch_url` and strips the prefix before `tools/call` reaches the server. Cross-server tool-name collisions within a loop now fail the loop at session initialization with a message naming the colliding servers — never resolved silently.
103
+ - **`session_recovery: :auto | :fail` per server:** `:auto` (default) keeps today's behavior — one transparent re-initialize on session loss; `:fail` fails the tool call loudly instead, for servers where the session is expensive server-side state (e.g. a sandbox container per session).
104
+ - **`transport_kind` on `solid_loop_mcp_sessions`** (`http` / `local` / `stdio` / `custom`, default `http`) — added to the init migration during the pre-release schema window and shown in the admin sessions list, session page, and Inspector. The Inspector works over local transports.
105
+ - **`Mcp::StdioTransport` (restricted contract):** child-process MCP servers over stdin/stdout with a spawn-per-call lifecycle (spawn → `initialize` handshake → request → graceful shutdown, kill on timeout, all inside one `deliver`). Stateless servers only — the session id is synthetic and implies no server-side continuity; this is a deliberate consequence of the durable-async core. Options: `command:` (argv array, never a shell string), `env:`, `cwd:`, `timeout:` (per call, includes spawn+handshake). stderr is captured into the wire log via the new optional `Result#stderr`; non-JSON stdout lines are kept as diagnostics instead of failing the call.
106
+
107
+ #### Storage prep
108
+ - `events.request_data` / `events.response_data` are now nullable in the init migration — preparation for a future wire-log storage change that will stop persisting these payloads inline.
109
+
110
+ #### Changed
111
+ - **`McpClient` is now a thin protocol layer:** it builds JSON-RPC envelopes and delegates I/O to a transport (`transport:` keyword; defaults to `Mcp::HttpTransport` built from `url`/`token`/`headers`). Public API (`initialize_session`, `list_tools`, `call_tool`) is unchanged.
112
+ - **Wire logs decoupled from Faraday:** `McpSessionInitializer` and `McpToolExecutionService` record Events from `Result#raw_request`/`raw_response`/`status`, and `McpClient#wire_log` renders the same data — identical observability for every transport. `McpClient#last_response` (a Faraday response) is replaced by `McpClient#last_result` (an `Mcp::Result`). Event logs no longer include HTTP headers, only the JSON-RPC envelopes and status.
113
+ - **`McpClient` raises `McpError` on JSON-RPC protocol-error envelopes** (`{"error": {...}}` with a 2xx status) for `initialize` / `tools/list` / `tools/call` instead of silently returning `[]` or a pretty-printed envelope marked successful. Non-Hash response bodies no longer crash the client; `session_recovery:` is validated eagerly at client construction.
114
+
115
+ ## [0.0.2] - 2026-07-08 (internal milestone — not published to RubyGems)
116
+
117
+ #### Added
118
+ - **Admin Dashboard (Web UI):** Mountable admin area for observing agents in the browser.
119
+ - Dashboard overview plus index/show screens for `Loops`, `Messages`, `ToolCalls`, `Events`, `McpSessions`, and `McpTools`.
120
+ - Query objects (`SolidLoop::Admin::*Query`) encapsulating filtering, ordering, and pagination, with a shared `_pagination` partial.
121
+ - Standardized `admin` layout built on reusable CSS utility classes.
122
+ - Charts rendered from vendored, self-hosted assets (Chart.js + Chartkick) — no external CDN, works offline and under a strict Content-Security-Policy.
123
+ - **MCP Inspector:** Interactive tool runner in the admin UI to invoke MCP tools ad‑hoc and replay previous tool calls, streaming results back via Turbo Streams (`run_tool`).
124
+ - **Multi-Provider LLM Dialects:** Provider-specific request/response translation so agents can target different backends.
125
+ - `Dialects::Anthropic` — native `/v1/messages` support: system-prompt extraction, `thinking` (reasoning) blocks, `tool_use`/`tool_result` mapping, and tool-result merging.
126
+ - `Dialects::Gemini` and refactored `Dialects::OpenAi`.
127
+ - `LlmUsageParser::Anthropic` for provider-native token/usage telemetry.
128
+ - **Live Observability:** `ObserveBroadcastJob` broadcasts newly created messages to a per-loop Turbo Stream channel (gated by `observe_enabled?`) for real-time UI updates.
129
+ - **Multi-Rails Testing:** `Appraisal`-based gemfiles for Rails 7.1, 7.2, and 8.1.
130
+ - **Test Infrastructure:** Full RSpec suite with per-provider integration specs (agent loops for OpenAI/Anthropic/Gemini, error recovery, legacy XML tool calls), LLM emulators (streaming, Anthropic, Gemini), an MCP emulator, and factories.
131
+ - **Coverage Tooling:** `lib/tasks/coverage.rake`, contributor coverage docs, and expanded CI workflow.
132
+
133
+ #### Changed
134
+ - **Improved Tool Error Feedback:**
135
+ - Unknown/unresolvable tool calls now return a structured message listing all available tools (native + MCP) so the model can self-correct.
136
+ - Invalid JSON in tool-call arguments now yields a clear re-prompt with a column pointer to the syntax error, and automatically re-queues an `LlmCompletionJob` so the loop can retry instead of stalling.
137
+
138
+ ## [0.0.1] - 2026-04-12 (internal milestone)
139
+ ### Initial MVP
140
+
141
+ #### Added
142
+ - **Core Architecture:** `ActiveJob` + `PostgreSQL` foundation for long-lived, autonomous AI agents.
143
+ - **Middleware System (Rack-style):**
144
+ - **LLM Middlewares:** Complete pipeline for `LlmCompletionJob` including `ErrorHandling`, `AgentInitialization`, `MessageBuilding`, `EventLogging`, `NetworkCalling`, and `ResponseParsing`.
145
+ - **Tool Middlewares:** Complete pipeline for `ToolExecutionJob` including `ErrorHandling`, `AgentInitialization`, `EventLogging`, `ToolExecution`, and `ResponseCreation`.
146
+ - **Pipeline Builder DSL:** Allow Host applications and Agents to dynamically mutate pipelines (`insert_before`, `insert_after`, `use`, `replace`, `delete`).
147
+ - **Resiliency:** Robust `ErrorHandling` to catch network timeouts and gracefully fail loops while preserving state and checkpoints in PostgreSQL.
148
+ - **Dual Tooling Protocol:**
149
+ - Internal: Native Ruby object method invocation via `allowed_tools`.
150
+ - External: Model Context Protocol (MCP) Streamable HTTP Server integration (`McpClient`).
151
+ - **Token Telemetry:** Aggregation and computation of TTFT (Time To First Token), TPS (Tokens Per Second), Prompt caching, and reasoning metadata via `LlmUsageParser`.
152
+ - **Event Logging:** Captures raw TCP/IP payloads and MCP interaction states for observability and debugging.
153
+ - **Payload Normalization:** Included optional `ToolCallXmlParser` for legacy open-source models generating `<tool_call>` XML blocks (disabled by default).
154
+
155
+ #### Fixed
156
+ - **N+1 Queries:** Added `inverse_of` to associations across `Loop`, `Message`, `ToolCall`, `Event`, `McpSession`, and `McpTool` models to ensure objects are correctly linked in memory during preloading.
@@ -0,0 +1,67 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ - Demonstrating empathy and kindness toward other people
21
+ - Being respectful of differing opinions, viewpoints, and experiences
22
+ - Giving and gracefully accepting constructive feedback
23
+ - Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ - Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ - The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ - Trolling, insulting or derogatory comments, and personal or political attacks
33
+ - Public or private harassment
34
+ - Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ - Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies within all community spaces, and also applies when
49
+ an individual is officially representing the community in public spaces.
50
+
51
+ ## Enforcement
52
+
53
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
54
+ reported to the community leaders responsible for enforcement at
55
+ **ru.ruslan@gmail.com**. All complaints will be reviewed and investigated promptly
56
+ and fairly.
57
+
58
+ All community leaders are obligated to respect the privacy and security of the
59
+ reporter of any incident.
60
+
61
+ ## Attribution
62
+
63
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
64
+ version 2.1, available at
65
+ <https://www.contributor-covenant.org/version/2/1/code_of_conduct.html>.
66
+
67
+ [homepage]: https://www.contributor-covenant.org
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,82 @@
1
+ # Contributing to SolidLoop
2
+
3
+ Thanks for your interest in improving SolidLoop! This document explains how to
4
+ set up the project locally, run the test suite, and submit changes.
5
+
6
+ By participating in this project you agree to abide by our
7
+ [Code of Conduct](CODE_OF_CONDUCT.md).
8
+
9
+ ## Getting started
10
+
11
+ SolidLoop is a Rails engine gem backed by ActiveJob + PostgreSQL. You will need:
12
+
13
+ - Ruby >= 3.1
14
+ - PostgreSQL (running locally and reachable by the dummy app)
15
+ - Bundler
16
+
17
+ Clone the repo and install dependencies:
18
+
19
+ ```bash
20
+ git clone git@github.com:ruslan/solid_loop.git
21
+ cd solid_loop
22
+ bundle install
23
+ ```
24
+
25
+ Prepare the test database (one-time):
26
+
27
+ ```bash
28
+ cd test/dummy && RAILS_ENV=test bundle exec rails db:create db:schema:load && cd -
29
+ ```
30
+
31
+ ## Running the tests
32
+
33
+ The default suite runs against the current Rails version:
34
+
35
+ ```bash
36
+ bundle exec rspec
37
+ # or
38
+ make test
39
+ ```
40
+
41
+ We support Rails 7.1, 7.2, and 8.1 via [Appraisal](https://github.com/thoughtbot/appraisal).
42
+ Install the appraisal gemfiles and run the suite across all supported versions:
43
+
44
+ ```bash
45
+ bundle exec appraisal install
46
+ bundle exec appraisal rspec
47
+ ```
48
+
49
+ ## Test coverage
50
+
51
+ Coverage is collected with SimpleCov (HTML + JSON formatters). After running the
52
+ suite, `coverage/coverage.json` is generated. Analyze it with the bundled Rake task:
53
+
54
+ ```bash
55
+ bundle exec rake coverage:report # full report, worst-covered first
56
+ bundle exec rake coverage:report'[partial]' # partially-covered files with line numbers
57
+ bundle exec rake coverage:report'[,,branch]' # coverage of lines changed on this branch
58
+ ```
59
+
60
+ New code should ship with tests. Please check that your changed lines are covered
61
+ before opening a pull request.
62
+
63
+ ## Submitting changes
64
+
65
+ 1. Fork the repository and create a topic branch from the default branch.
66
+ 2. Make your change with accompanying tests.
67
+ 3. Ensure the full suite is green across supported Rails versions.
68
+ 4. Update `CHANGELOG.md` under an `## [Unreleased]` heading describing your change.
69
+ 5. Open a pull request with a clear description of the motivation and approach.
70
+
71
+ ## Reporting bugs
72
+
73
+ Open an issue with:
74
+
75
+ - The SolidLoop version, Ruby version, and Rails version.
76
+ - A minimal reproduction (a failing spec is ideal).
77
+ - Expected vs. actual behavior, including any relevant backtrace.
78
+
79
+ ## Coding style
80
+
81
+ Match the style of the surrounding code. Keep changes focused — unrelated
82
+ refactors are easier to review as separate pull requests.
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Ruslan Mukhametov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.