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
@@ -0,0 +1,142 @@
1
+ <div class="sl-page-header">
2
+ <div>
3
+ <h1 class="sl-page-header__title">MCP Session #<%= @mcp_session.id %></h1>
4
+ <p class="sl-page-header__meta">Name: <strong><%= @mcp_session.mcp_name %></strong> | Transport: <strong><%= @mcp_session.transport_kind %></strong> | Session: <%= @mcp_session.session_id %></p>
5
+ </div>
6
+ <div class="sl-page-header__actions">
7
+ <%= link_to "Back to Loop", loop_path(@mcp_session.loop_id), class: "sl-btn" %>
8
+ <%= link_to "Open Inspector", inspector_mcp_session_path(@mcp_session), class: "sl-btn sl-btn--primary" %>
9
+ </div>
10
+ </div>
11
+
12
+ <div class="sl-layout-split">
13
+ <div class="sl-layout-split__main">
14
+ <div class="sl-card">
15
+ <h3>Discovered Tools</h3>
16
+ <% if @mcp_tools.any? %>
17
+ <table class="sl-table sl-mt-10">
18
+ <thead>
19
+ <tr>
20
+ <th class="sl-table__th">Name</th>
21
+ <th class="sl-table__th">Description</th>
22
+ <th class="sl-table__th"></th>
23
+ </tr>
24
+ </thead>
25
+ <tbody>
26
+ <% @mcp_tools.each do |tool| %>
27
+ <tr>
28
+ <td class="sl-table__td"><strong><%= tool.name %></strong></td>
29
+ <td class="sl-table__td"><%= tool.description.presence || "-" %></td>
30
+ <td class="sl-table__td" style="text-align: right;">
31
+ <%= link_to "View", mcp_tool_path(tool), class: "sl-btn sl-btn--small" %>
32
+ </td>
33
+ </tr>
34
+ <% end %>
35
+ </tbody>
36
+ </table>
37
+ <% else %>
38
+ <p class="sl-mt-10" style="color: var(--sl-muted-text);">No tools discovered for this session.</p>
39
+ <% end %>
40
+ </div>
41
+
42
+ <% discovered_prompts = @mcp_session.metadata&.fetch("prompts", []) || [] %>
43
+ <% if discovered_prompts.any? %>
44
+ <div class="sl-card">
45
+ <h3>Discovered Prompts</h3>
46
+ <table class="sl-table sl-mt-10">
47
+ <thead>
48
+ <tr>
49
+ <th class="sl-table__th">Name</th>
50
+ <th class="sl-table__th">Description</th>
51
+ <th class="sl-table__th">Arguments</th>
52
+ </tr>
53
+ </thead>
54
+ <tbody>
55
+ <% discovered_prompts.each do |prompt| %>
56
+ <tr>
57
+ <td class="sl-table__td"><strong><%= prompt["name"] %></strong></td>
58
+ <td class="sl-table__td"><%= prompt["description"].presence || "-" %></td>
59
+ <td class="sl-table__td"><%= Array(prompt["arguments"]).map { |a| a["name"] }.join(", ").presence || "-" %></td>
60
+ </tr>
61
+ <% end %>
62
+ </tbody>
63
+ </table>
64
+ </div>
65
+ <% end %>
66
+
67
+ <% discovered_resources = @mcp_session.metadata&.fetch("resources", []) || [] %>
68
+ <% if discovered_resources.any? %>
69
+ <div class="sl-card">
70
+ <h3>Discovered Resources</h3>
71
+ <table class="sl-table sl-mt-10">
72
+ <thead>
73
+ <tr>
74
+ <th class="sl-table__th">URI</th>
75
+ <th class="sl-table__th">Name</th>
76
+ <th class="sl-table__th">MIME</th>
77
+ </tr>
78
+ </thead>
79
+ <tbody>
80
+ <% discovered_resources.each do |resource| %>
81
+ <tr>
82
+ <td class="sl-table__td"><%= resource["uri"] %></td>
83
+ <td class="sl-table__td"><%= resource["name"].presence || "-" %></td>
84
+ <td class="sl-table__td"><%= resource["mimeType"].presence || "-" %></td>
85
+ </tr>
86
+ <% end %>
87
+ </tbody>
88
+ </table>
89
+ </div>
90
+ <% end %>
91
+
92
+ <div class="sl-card">
93
+ <h3>Recent Tool Calls</h3>
94
+ <% if @recent_tool_calls.any? %>
95
+ <table class="sl-table sl-mt-10">
96
+ <thead>
97
+ <tr>
98
+ <th class="sl-table__th">ID</th>
99
+ <th class="sl-table__th">Function</th>
100
+ <th class="sl-table__th">Status</th>
101
+ <th class="sl-table__th">Created</th>
102
+ <th class="sl-table__th"></th>
103
+ </tr>
104
+ </thead>
105
+ <tbody>
106
+ <% @recent_tool_calls.each do |tool_call| %>
107
+ <tr>
108
+ <td class="sl-table__td"><%= tool_call.id %></td>
109
+ <td class="sl-table__td"><%= tool_call.function_name %></td>
110
+ <td class="sl-table__td">
111
+ <span class="sl-badge sl-badge--<%= tool_call.is_success ? 'success' : 'error' %>"><%= tool_call.is_success ? 'SUCCESS' : 'FAILED' %></span>
112
+ </td>
113
+ <td class="sl-table__td"><%= tool_call.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
114
+ <td class="sl-table__td" style="text-align: right;">
115
+ <%= link_to "View", tool_call_path(tool_call), class: "sl-btn sl-btn--small" %>
116
+ </td>
117
+ </tr>
118
+ <% end %>
119
+ </tbody>
120
+ </table>
121
+ <% else %>
122
+ <p class="sl-mt-10" style="color: var(--sl-muted-text);">No tool calls have used this session yet.</p>
123
+ <% end %>
124
+ </div>
125
+ </div>
126
+
127
+ <div class="sl-layout-split__sidebar">
128
+ <div class="sl-card">
129
+ <h3>Relations</h3>
130
+ <div class="sl-mt-10" style="display: flex; flex-direction: column; gap: 8px;">
131
+ <%= link_to "Loop Details", loop_path(@mcp_session.loop_id), class: "sl-btn sl-btn--small" %>
132
+ <%= link_to "Loop Events", events_path(loop_id: @mcp_session.loop_id), class: "sl-btn sl-btn--small" %>
133
+ <%= link_to "Session Inspector", inspector_mcp_session_path(@mcp_session), class: "sl-btn sl-btn--small sl-btn--primary" %>
134
+ </div>
135
+ </div>
136
+
137
+ <div class="sl-card">
138
+ <h3>Metadata</h3>
139
+ <div class="sl-json sl-mt-10"><%= JSON.pretty_generate(@mcp_session.metadata || {}) rescue @mcp_session.metadata %></div>
140
+ </div>
141
+ </div>
142
+ </div>
@@ -0,0 +1,44 @@
1
+ <div class="sl-card">
2
+ <%= form_with url: mcp_tools_path, method: :get, class: "sl-page-header__actions", style: "justify-content: flex-start; flex-wrap: wrap;" do |f| %>
3
+ <div style="display: flex; align-items: center; gap: 8px;">
4
+ <span class="sl-stat-card__label">Name:</span>
5
+ <%= f.text_field :name, value: params[:name], class: "sl-btn sl-btn--small", placeholder: "list_files" %>
6
+ </div>
7
+ <div style="display: flex; align-items: center; gap: 8px; margin-left: 20px;">
8
+ <span class="sl-stat-card__label">Session:</span>
9
+ <%= f.text_field :mcp_session_id, value: params[:mcp_session_id], class: "sl-btn sl-btn--small", style: "width: 80px;", placeholder: "ID" %>
10
+ </div>
11
+ <%= f.submit "Filter", class: "sl-btn sl-btn--small sl-btn--primary", style: "margin-left: 12px;" %>
12
+ <% if params[:name].present? || params[:mcp_session_id].present? %>
13
+ <%= link_to "Clear Filters", mcp_tools_path, class: "sl-btn sl-btn--small", style: "margin-left: 12px;" %>
14
+ <% end %>
15
+ <% end %>
16
+ </div>
17
+
18
+ <table class="sl-table">
19
+ <thead>
20
+ <tr>
21
+ <th class="sl-table__th">ID</th>
22
+ <th class="sl-table__th">Name</th>
23
+ <th class="sl-table__th">Session</th>
24
+ <th class="sl-table__th">Created At</th>
25
+ <th class="sl-table__th"></th>
26
+ </tr>
27
+ </thead>
28
+ <tbody>
29
+ <% @mcp_tools.each do |tool| %>
30
+ <tr>
31
+ <td class="sl-table__td"><%= tool.id %></td>
32
+ <td class="sl-table__td"><%= tool.name %></td>
33
+ <td class="sl-table__td"><%= link_to tool.mcp_session_id, mcp_session_path(tool.mcp_session_id) if tool.mcp_session_id %></td>
34
+ <td class="sl-table__td"><%= tool.created_at.strftime("%Y-%m-%d %H:%M") %></td>
35
+ <td class="sl-table__td" style="text-align: right; white-space: nowrap;">
36
+ <%= link_to "Inspector", inspector_mcp_session_path(tool.mcp_session_id, tool_name: tool.name), class: "sl-btn sl-btn--small sl-btn--primary" %>
37
+ <%= link_to "View", mcp_tool_path(tool), class: "sl-btn sl-btn--small" %>
38
+ </td>
39
+ </tr>
40
+ <% end %>
41
+ </tbody>
42
+ </table>
43
+
44
+ <%= render "solid_loop/shared/pagination", pagination: @pagination %>
@@ -0,0 +1,69 @@
1
+ <div class="sl-page-header">
2
+ <div>
3
+ <h1 class="sl-page-header__title">MCP Tool #<%= @mcp_tool.id %></h1>
4
+ <p class="sl-page-header__meta">Name: <strong><%= @mcp_tool.name %></strong> | Session: <%= @mcp_tool.mcp_session.session_id %></p>
5
+ </div>
6
+ <div class="sl-page-header__actions">
7
+ <%= link_to "Back to Session", mcp_session_path(@mcp_tool.mcp_session_id), class: "sl-btn" %>
8
+ <%= link_to "Open Inspector", inspector_mcp_session_path(@mcp_tool.mcp_session_id, tool_name: @mcp_tool.name), class: "sl-btn sl-btn--primary" %>
9
+ </div>
10
+ </div>
11
+
12
+ <div class="sl-layout-split">
13
+ <div class="sl-layout-split__main">
14
+ <div class="sl-card">
15
+ <h3>Description</h3>
16
+ <div class="sl-json sl-mt-10"><%= @mcp_tool.description.presence || "No description" %></div>
17
+ </div>
18
+
19
+ <div class="sl-card">
20
+ <h3>Input Schema</h3>
21
+ <div class="sl-json sl-mt-10"><%= JSON.pretty_generate(@mcp_tool.input_schema || {}) rescue @mcp_tool.input_schema %></div>
22
+ </div>
23
+
24
+ <div class="sl-card">
25
+ <h3>Recent Calls Using This Tool</h3>
26
+ <% if @recent_tool_calls.any? %>
27
+ <table class="sl-table sl-mt-10">
28
+ <thead>
29
+ <tr>
30
+ <th class="sl-table__th">ID</th>
31
+ <th class="sl-table__th">Status</th>
32
+ <th class="sl-table__th">Duration</th>
33
+ <th class="sl-table__th">Created</th>
34
+ <th class="sl-table__th"></th>
35
+ </tr>
36
+ </thead>
37
+ <tbody>
38
+ <% @recent_tool_calls.each do |tool_call| %>
39
+ <tr>
40
+ <td class="sl-table__td"><%= tool_call.id %></td>
41
+ <td class="sl-table__td">
42
+ <span class="sl-badge sl-badge--<%= tool_call.is_success ? 'success' : 'error' %>"><%= tool_call.is_success ? 'SUCCESS' : 'FAILED' %></span>
43
+ </td>
44
+ <td class="sl-table__td"><%= number_with_precision(tool_call.duration, precision: 3) %>s</td>
45
+ <td class="sl-table__td"><%= tool_call.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
46
+ <td class="sl-table__td" style="text-align: right;">
47
+ <%= link_to "View", tool_call_path(tool_call), class: "sl-btn sl-btn--small" %>
48
+ </td>
49
+ </tr>
50
+ <% end %>
51
+ </tbody>
52
+ </table>
53
+ <% else %>
54
+ <p class="sl-mt-10" style="color: var(--sl-muted-text);">No tool calls have used this MCP tool yet.</p>
55
+ <% end %>
56
+ </div>
57
+ </div>
58
+
59
+ <div class="sl-layout-split__sidebar">
60
+ <div class="sl-card">
61
+ <h3>Relations</h3>
62
+ <div class="sl-mt-10" style="display: flex; flex-direction: column; gap: 8px;">
63
+ <%= link_to "MCP Session", mcp_session_path(@mcp_tool.mcp_session_id), class: "sl-btn sl-btn--small" %>
64
+ <%= link_to "Loop Details", loop_path(@mcp_tool.mcp_session.loop_id), class: "sl-btn sl-btn--small" %>
65
+ <%= link_to "Open Inspector", inspector_mcp_session_path(@mcp_tool.mcp_session_id, tool_name: @mcp_tool.name), class: "sl-btn sl-btn--small sl-btn--primary" %>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </div>
@@ -0,0 +1,75 @@
1
+ <div id="sl-message-<%= message.id %>" class="sl-card sl-message sl-message--<%= message.role %>">
2
+ <div class="sl-message__header">
3
+ <strong class="sl-message__role">
4
+ <%= message.role %>
5
+ </strong>
6
+ <% model_name = message.metadata&.dig("model") %>
7
+ <% if model_name.present? %>
8
+ <span class="sl-badge sl-badge--pending" title="Model that served this turn" style="margin-left: 6px; font-weight: normal;">
9
+ <%= model_name %>
10
+ </span>
11
+ <% end %>
12
+ <span class="sl-message__time">
13
+ <%= message.created_at.strftime("%H:%M:%S") %>
14
+ </span>
15
+ </div>
16
+
17
+ <div class="sl-json">
18
+ <% if message.content.present? %>
19
+ <%= message.content %>
20
+ <% end %>
21
+
22
+ <% if message.tool_calls_raw.present? || message.tool_calls.any? %>
23
+ <div class="sl-message__tools">
24
+ <strong>Tools:</strong>
25
+ <div class="sl-mt-10">
26
+ <% (message.tool_calls.any? ? message.tool_calls : message.tool_calls_raw).each do |tc| %>
27
+ <%
28
+ name = tc.is_a?(SolidLoop::ToolCall) ? tc.function_name : tc.dig("function", "name")
29
+ tc_id = tc.is_a?(SolidLoop::ToolCall) ? tc.id : nil
30
+ %>
31
+ <div class="sl-badge <%= tc.is_a?(SolidLoop::ToolCall) && tc.is_success ? 'sl-badge--success' : 'sl-badge--pending' %>" style="margin-bottom: 5px;">
32
+ <% if tc_id %>
33
+ <%= link_to name, tool_call_path(tc_id), style: "color: inherit; text-decoration: none;" %>
34
+ <% else %>
35
+ <%= name %>
36
+ <% end %>
37
+ </div>
38
+ <% end %>
39
+ </div>
40
+ </div>
41
+ <% end %>
42
+
43
+ <% if message.reasoning_content.present? %>
44
+ <div class="sl-message__reasoning">
45
+ <em>Thinking:</em>
46
+ <pre class="sl-pre-wrap"><%= message.reasoning_content %></pre>
47
+ </div>
48
+ <% end %>
49
+
50
+ <div class="sl-message__footer">
51
+ <div class="sl-message__footer-item">
52
+ Tokens: <span class="sl-message__footer-value">P<%= message.tokens_prompt %>+C<%= message.tokens_completion %></span>
53
+ <% if message.tokens_prompt_cached > 0 %>
54
+ <span style="color: var(--sl-success); font-size: 0.9em; margin-left: 4px;">(cached <%= message.tokens_prompt_cached %>)</span>
55
+ <% end %>
56
+ </div>
57
+ <div class="sl-message__footer-item">
58
+ Cost: <span class="sl-message__footer-value"><%= sl_money(message.cost) %></span>
59
+ </div>
60
+ <% if message.tps > 0 %>
61
+ <div class="sl-message__footer-item">
62
+ TPS: <span class="sl-message__footer-value"><%= message.tps %></span>
63
+ </div>
64
+ <% end %>
65
+ <% if message.ttft > 0 %>
66
+ <div class="sl-message__footer-item">
67
+ TTFT: <span class="sl-message__footer-value"><%= number_with_precision(message.ttft, precision: 2) %>s</span>
68
+ </div>
69
+ <% end %>
70
+ <div class="sl-message__footer-item" style="margin-left: auto;">
71
+ <%= link_to "Details", message_path(message), class: "sl-btn sl-btn--small" %>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </div>
@@ -0,0 +1,80 @@
1
+ <div class="sl-page-header">
2
+ <h2>Messages</h2>
3
+ </div>
4
+
5
+ <div class="sl-card">
6
+ <%= form_with url: messages_path, method: :get, class: "sl-page-header__actions", style: "justify-content: flex-start; flex-wrap: wrap;" do |f| %>
7
+ <div style="display: flex; align-items: center; gap: 8px;">
8
+ <span class="sl-stat-card__label">Role:</span>
9
+ <%= f.select :role,
10
+ options_for_select([["All Roles", ""]] + SolidLoop::Message.roles.keys.map { |r| [r.upcase, r] }, params[:role]),
11
+ {},
12
+ class: "sl-btn sl-btn--small",
13
+ onchange: "this.form.submit()" %>
14
+ </div>
15
+
16
+ <div style="display: flex; align-items: center; gap: 8px; margin-left: 20px;">
17
+ <span class="sl-stat-card__label">Loop ID:</span>
18
+ <%= f.text_field :loop_id, value: params[:loop_id], class: "sl-btn sl-btn--small", style: "width: 80px;", placeholder: "ID" %>
19
+ <%= f.submit "Filter", class: "sl-btn sl-btn--small sl-btn--primary" %>
20
+ </div>
21
+
22
+ <label class="sl-stat-card__label" style="display: flex; align-items: center; gap: 6px; margin-left: 20px;">
23
+ <%= f.check_box :has_tool_call, { checked: params[:has_tool_call] == "1", onchange: "this.form.submit()" }, "1", "0" %>
24
+ Tool messages
25
+ </label>
26
+
27
+ <label class="sl-stat-card__label" style="display: flex; align-items: center; gap: 6px; margin-left: 12px;">
28
+ <%= f.check_box :has_reasoning, { checked: params[:has_reasoning] == "1", onchange: "this.form.submit()" }, "1", "0" %>
29
+ Reasoning
30
+ </label>
31
+
32
+ <% if params[:role].present? || params[:loop_id].present? || params[:has_tool_call] == "1" || params[:has_reasoning] == "1" %>
33
+ <%= link_to "Clear Filters", messages_path, class: "sl-btn sl-btn--small", style: "margin-left: 20px;" %>
34
+ <% end %>
35
+ <% end %>
36
+ </div>
37
+
38
+ <table class="sl-table">
39
+ <thead>
40
+ <tr>
41
+ <th class="sl-table__th">ID</th>
42
+ <th class="sl-table__th">Loop</th>
43
+ <th class="sl-table__th">Role</th>
44
+ <th class="sl-table__th">Model</th>
45
+ <th class="sl-table__th">Tokens</th>
46
+ <th class="sl-table__th">Cost</th>
47
+ <th class="sl-table__th">Created At</th>
48
+ <th class="sl-table__th"></th>
49
+ </tr>
50
+ </thead>
51
+ <tbody>
52
+ <% @messages.each do |message| %>
53
+ <tr>
54
+ <td class="sl-table__td"><%= message.id %></td>
55
+ <td class="sl-table__td">
56
+ <%= link_to "Loop ##{message.loop_id}", loop_path(message.loop_id), style: "color: inherit;" if message.loop_id %>
57
+ </td>
58
+ <td class="sl-table__td">
59
+ <span class="sl-badge sl-badge--<%=
60
+ case message.role
61
+ when 'user' then 'success'
62
+ when 'assistant' then 'processing'
63
+ when 'tool' then 'pending'
64
+ else 'queued'
65
+ end
66
+ %>"><%= message.role.upcase %></span>
67
+ </td>
68
+ <td class="sl-table__td"><%= message.metadata&.dig("model").presence || "—" %></td>
69
+ <td class="sl-table__td">P<%= message.tokens_prompt %>+C<%= message.tokens_completion %></td>
70
+ <td class="sl-table__td"><%= sl_money(message.cost) %></td>
71
+ <td class="sl-table__td"><%= message.created_at.strftime("%Y-%m-%d %H:%M") %></td>
72
+ <td class="sl-table__td" style="text-align: right;">
73
+ <%= link_to "View", message_path(message), class: "sl-btn sl-btn--small" %>
74
+ </td>
75
+ </tr>
76
+ <% end %>
77
+ </tbody>
78
+ </table>
79
+
80
+ <%= render "solid_loop/shared/pagination", pagination: @pagination %>
@@ -0,0 +1,121 @@
1
+ <div class="sl-page-header">
2
+ <div>
3
+ <h1 class="sl-page-header__title">Message #<%= @message.id %></h1>
4
+ <p class="sl-page-header__meta">
5
+ Loop #<%= @message.loop_id %> | Role: <strong><%= @message.role.upcase %></strong>
6
+ <% if (model_name = @message.metadata&.dig("model")).present? %>
7
+ | Model: <strong><%= model_name %></strong>
8
+ <% end %>
9
+ </p>
10
+ </div>
11
+ <div class="sl-page-header__actions">
12
+ <%= link_to "Back to Loop", loop_path(@message.loop_id), class: "sl-btn" %>
13
+ </div>
14
+ </div>
15
+
16
+ <div class="sl-layout-split">
17
+ <div class="sl-layout-split__main">
18
+ <div class="sl-card">
19
+ <h3>Content</h3>
20
+ <div class="sl-json sl-mt-10 sl-pre-wrap"><%= @message.content %></div>
21
+ </div>
22
+
23
+ <% if @message.reasoning_content.present? %>
24
+ <div class="sl-card">
25
+ <h3>Reasoning</h3>
26
+ <div class="sl-json sl-mt-10 sl-pre-wrap" style="color: var(--sl-muted-text); font-style: italic;"><%= @message.reasoning_content %></div>
27
+ </div>
28
+ <% end %>
29
+
30
+ <% if @message.tool_calls_raw.present? %>
31
+ <div class="sl-card">
32
+ <h3>Raw Tool Calls</h3>
33
+ <div class="sl-json sl-mt-10"><%= JSON.pretty_generate(@message.tool_calls_raw) %></div>
34
+ </div>
35
+ <% end %>
36
+
37
+ <% if @message.tool_calls.any? %>
38
+ <div class="sl-card">
39
+ <h3>Associated Tool Calls</h3>
40
+ <table class="sl-table sl-mt-10">
41
+ <thead>
42
+ <tr>
43
+ <th class="sl-table__th">ID</th>
44
+ <th class="sl-table__th">Function</th>
45
+ <th class="sl-table__th">Status</th>
46
+ <th class="sl-table__th">Duration</th>
47
+ <th class="sl-table__th"></th>
48
+ </tr>
49
+ </thead>
50
+ <tbody>
51
+ <% @message.tool_calls.each do |tc| %>
52
+ <tr>
53
+ <td class="sl-table__td"><%= tc.id %></td>
54
+ <td class="sl-table__td"><strong><%= tc.function_name %></strong></td>
55
+ <td class="sl-table__td">
56
+ <span class="sl-badge sl-badge--<%= tc.is_success ? 'success' : 'error' %>">
57
+ <%= tc.is_success ? 'SUCCESS' : 'FAILED' %>
58
+ </span>
59
+ </td>
60
+ <td class="sl-table__td"><%= number_with_precision(tc.duration, precision: 3) %>s</td>
61
+ <td class="sl-table__td" style="text-align: right;">
62
+ <%= link_to "View", tool_call_path(tc), class: "sl-btn sl-btn--small" %>
63
+ </td>
64
+ </tr>
65
+ <% end %>
66
+ </tbody>
67
+ </table>
68
+ </div>
69
+ <% end %>
70
+ </div>
71
+
72
+ <div class="sl-layout-split__sidebar">
73
+ <div class="sl-card">
74
+ <h3>Message Metrics</h3>
75
+ <div class="sl-mt-10">
76
+ <% if (model_name = @message.metadata&.dig("model")).present? %>
77
+ <div class="sl-message__footer-item">
78
+ Model: <span class="sl-message__footer-value"><%= model_name %></span>
79
+ </div>
80
+ <% end %>
81
+ <div class="sl-message__footer-item">
82
+ Prompt Tokens: <span class="sl-message__footer-value"><%= @message.tokens_prompt %></span>
83
+ </div>
84
+ <div class="sl-message__footer-item">
85
+ Completion Tokens: <span class="sl-message__footer-value"><%= @message.tokens_completion %></span>
86
+ </div>
87
+ <% if @message.tokens_prompt_cached > 0 %>
88
+ <div class="sl-message__footer-item" style="color: var(--sl-success);">
89
+ Cached Tokens: <span class="sl-message__footer-value" style="color: var(--sl-success);"><%= @message.tokens_prompt_cached %></span>
90
+ </div>
91
+ <% end %>
92
+ <div class="sl-message__footer-item">
93
+ Cost: <span class="sl-message__footer-value"><%= sl_money(@message.cost) %></span>
94
+ </div>
95
+
96
+ <hr style="border: 0; border-top: 1px solid var(--sl-border-color); margin: 10px 0;">
97
+
98
+ <div class="sl-message__footer-item">
99
+ TTFT: <span class="sl-message__footer-value"><%= number_with_precision(@message.ttft, precision: 3) %>s</span>
100
+ </div>
101
+ <div class="sl-message__footer-item">
102
+ TPS: <span class="sl-message__footer-value"><%= @message.tps %></span>
103
+ </div>
104
+ <div class="sl-message__footer-item">
105
+ Thinking: <span class="sl-message__footer-value"><%= number_with_precision(@message.duration_thinking, precision: 3) %>s</span>
106
+ </div>
107
+ <div class="sl-message__footer-item">
108
+ Generation: <span class="sl-message__footer-value"><%= number_with_precision(@message.duration_generation, precision: 3) %>s</span>
109
+ </div>
110
+ <div class="sl-message__footer-item">
111
+ Cumulative: <span class="sl-message__footer-value"><%= number_with_precision(@message.cumulative_duration, precision: 3) %>s</span>
112
+ </div>
113
+ </div>
114
+ </div>
115
+
116
+ <div class="sl-card">
117
+ <h3>Metadata</h3>
118
+ <div class="sl-json sl-mt-10"><%= JSON.pretty_generate(@message.metadata || {}) rescue @message.metadata %></div>
119
+ </div>
120
+ </div>
121
+ </div>
@@ -0,0 +1,21 @@
1
+ <% return unless pagination && (pagination[:prev_page] || pagination[:next_page] || pagination[:total_pages] > 1) %>
2
+
3
+ <div class="sl-pagination">
4
+ <div class="sl-pagination__info">
5
+ Page <%= pagination[:current_page] %>
6
+ <% if pagination[:total_pages].positive? %>
7
+ of <%= pagination[:total_pages] %>
8
+ <% end %>
9
+ (<%= pagination[:total_count] %> total)
10
+ </div>
11
+
12
+ <div class="sl-pagination__links">
13
+ <% if pagination[:prev_page] %>
14
+ <%= link_to "Previous", url_for(request.query_parameters.merge(page: pagination[:prev_page])), class: "sl-btn sl-btn--small" %>
15
+ <% end %>
16
+
17
+ <% if pagination[:next_page] %>
18
+ <%= link_to "Next", url_for(request.query_parameters.merge(page: pagination[:next_page])), class: "sl-btn sl-btn--small" %>
19
+ <% end %>
20
+ </div>
21
+ </div>
@@ -0,0 +1,53 @@
1
+ <div class="sl-card">
2
+ <%= form_with url: tool_calls_path, method: :get, class: "sl-page-header__actions", style: "justify-content: flex-start; flex-wrap: wrap;" do |f| %>
3
+ <div style="display: flex; align-items: center; gap: 8px;">
4
+ <span class="sl-stat-card__label">Loop ID:</span>
5
+ <%= f.text_field :loop_id, value: params[:loop_id], class: "sl-btn sl-btn--small", style: "width: 80px;", placeholder: "ID" %>
6
+ </div>
7
+ <div style="display: flex; align-items: center; gap: 8px; margin-left: 20px;">
8
+ <span class="sl-stat-card__label">Function:</span>
9
+ <%= f.text_field :function_name, value: params[:function_name], class: "sl-btn sl-btn--small", placeholder: "list_files" %>
10
+ </div>
11
+ <div style="display: flex; align-items: center; gap: 8px; margin-left: 20px;">
12
+ <span class="sl-stat-card__label">Status:</span>
13
+ <%= f.select :success,
14
+ options_for_select([["All", ""], ["Success", "success"], ["Failed", "failed"]], params[:success]),
15
+ {},
16
+ class: "sl-btn sl-btn--small",
17
+ onchange: "this.form.submit()" %>
18
+ </div>
19
+ <%= f.submit "Filter", class: "sl-btn sl-btn--small sl-btn--primary", style: "margin-left: 12px;" %>
20
+ <% if params[:loop_id].present? || params[:function_name].present? || params[:success].present? %>
21
+ <%= link_to "Clear Filters", tool_calls_path, class: "sl-btn sl-btn--small", style: "margin-left: 12px;" %>
22
+ <% end %>
23
+ <% end %>
24
+ </div>
25
+
26
+ <table class="sl-table">
27
+ <thead>
28
+ <tr>
29
+ <th class="sl-table__th">ID</th>
30
+ <th class="sl-table__th">Message ID</th>
31
+ <th class="sl-table__th">Function</th>
32
+ <th class="sl-table__th">Status</th>
33
+ <th class="sl-table__th">Created At</th>
34
+ <th class="sl-table__th">Actions</th>
35
+ </tr>
36
+ </thead>
37
+ <tbody>
38
+ <% @tool_calls.each do |tool_call| %>
39
+ <tr>
40
+ <td class="sl-table__td"><%= tool_call.id %></td>
41
+ <td class="sl-table__td"><%= link_to tool_call.message_id, message_path(tool_call.message_id) if tool_call.message_id %></td>
42
+ <td class="sl-table__td"><%= tool_call.function_name %></td>
43
+ <td class="sl-table__td"><span class="sl-badge sl-badge--<%= tool_call.error_message.present? ? 'error' : 'success' %>"><%= tool_call.error_message.present? ? 'failed' : 'success' %></span></td>
44
+ <td class="sl-table__td"><%= tool_call.created_at.strftime("%Y-%m-%d %H:%M") %></td>
45
+ <td class="sl-table__td">
46
+ <%= link_to "View", tool_call_path(tool_call), class: "sl-btn sl-btn--small" %>
47
+ </td>
48
+ </tr>
49
+ <% end %>
50
+ </tbody>
51
+ </table>
52
+
53
+ <%= render "solid_loop/shared/pagination", pagination: @pagination %>