insika 0.8.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +312 -2
  3. data/README.md +10 -6
  4. data/bin/insika +4 -1
  5. data/bin/insika-router +0 -1
  6. data/docs/AGENTS.md +142 -18
  7. data/docs/API.md +73 -0
  8. data/docs/ARCHITECTURE.md +56 -50
  9. data/docs/ARTIFACTS.md +33 -3
  10. data/docs/BENCHMARK.md +50 -7
  11. data/docs/CHANNELS.md +19 -2
  12. data/docs/CONTEXT.md +96 -27
  13. data/docs/CROSS-HARNESS-BENCH.md +157 -0
  14. data/docs/DEPLOY.md +70 -13
  15. data/docs/EMBEDDING.md +10 -0
  16. data/docs/EVALS.md +129 -10
  17. data/docs/FACTS.md +12 -6
  18. data/docs/HARVEST.md +8 -3
  19. data/docs/KNOWLEDGE.md +14 -0
  20. data/docs/MEDIA.md +6 -2
  21. data/docs/OBSERVABILITY.md +164 -28
  22. data/docs/PERFORMANCE.md +110 -0
  23. data/docs/PLUGINS.md +4 -4
  24. data/docs/POLICY.md +26 -14
  25. data/docs/RELEASING.md +9 -0
  26. data/docs/ROUTER.md +7 -0
  27. data/docs/RUBYLLM_2_MIGRATION.md +307 -0
  28. data/docs/SECURITY.md +34 -1
  29. data/docs/SHARED-CONVERSATIONS.md +40 -0
  30. data/docs/SKILLS.md +5 -3
  31. data/docs/STUDIO.md +122 -0
  32. data/docs/TOOLS.md +238 -42
  33. data/docs/operate.md +3 -0
  34. data/docs/prompts/ADD-TOOL.md +12 -2
  35. data/docs/prompts/DIAGNOSE-TURN.md +3 -0
  36. data/docs/prompts/GO-LIVE.md +3 -1
  37. data/lib/insika/agent_defaults.rb +140 -0
  38. data/lib/insika/agent_file_store.rb +12 -0
  39. data/lib/insika/agent_profile.rb +99 -17
  40. data/lib/insika/artifact_signing.rb +11 -0
  41. data/lib/insika/channels/web/widget.js +33 -0
  42. data/lib/insika/channels/web.rb +5 -2
  43. data/lib/insika/chat_builder.rb +124 -28
  44. data/lib/insika/checkpoint.rb +6 -1
  45. data/lib/insika/checkpoint_store.rb +59 -7
  46. data/lib/insika/commands/agent_payload.rb +3 -3
  47. data/lib/insika/commands/delete_kit.rb +26 -0
  48. data/lib/insika/commands/gate_harvest.rb +3 -0
  49. data/lib/insika/commands/review_guardrail_flag.rb +38 -0
  50. data/lib/insika/commands/run_distillation.rb +5 -8
  51. data/lib/insika/commands/run_harvest.rb +17 -1
  52. data/lib/insika/commands/seed_session.rb +128 -0
  53. data/lib/insika/commands/send_message.rb +9 -3
  54. data/lib/insika/commands/update_settings.rb +23 -1
  55. data/lib/insika/commands/write_kit.rb +27 -0
  56. data/lib/insika/compaction.rb +33 -7
  57. data/lib/insika/config_store.rb +87 -7
  58. data/lib/insika/context/builder.rb +29 -9
  59. data/lib/insika/context/priority.rb +5 -0
  60. data/lib/insika/context/provider.rb +7 -2
  61. data/lib/insika/context/providers/briefing.rb +11 -8
  62. data/lib/insika/context/providers/fence_notice.rb +27 -0
  63. data/lib/insika/context/providers/knowledge.rb +32 -6
  64. data/lib/insika/context/providers/memory.rb +61 -8
  65. data/lib/insika/context/providers/pending_confirmation.rb +52 -0
  66. data/lib/insika/context/providers/prompt.rb +17 -4
  67. data/lib/insika/context/providers/session.rb +46 -6
  68. data/lib/insika/context/reranker.rb +75 -0
  69. data/lib/insika/distill.rb +22 -13
  70. data/lib/insika/doctor.rb +142 -4
  71. data/lib/insika/dsl/runtime.rb +37 -4
  72. data/lib/insika/dsl/server_boot.rb +4 -12
  73. data/lib/insika/dsl.rb +19 -5
  74. data/lib/insika/edge_limiter.rb +27 -16
  75. data/lib/insika/egress_guard.rb +11 -3
  76. data/lib/insika/env_schema.rb +33 -73
  77. data/lib/insika/errors.rb +1 -0
  78. data/lib/insika/evals/assertions.rb +259 -18
  79. data/lib/insika/evals/golden.rb +230 -4
  80. data/lib/insika/evals/judge.rb +19 -10
  81. data/lib/insika/evals/moderator_compare.rb +47 -0
  82. data/lib/insika/evals/report.rb +61 -9
  83. data/lib/insika/evals/runner.rb +64 -4
  84. data/lib/insika/evals/simulator.rb +21 -3
  85. data/lib/insika/evals/transport.rb +317 -15
  86. data/lib/insika/event_stream.rb +63 -7
  87. data/lib/insika/evidence.rb +105 -17
  88. data/lib/insika/executor.rb +698 -164
  89. data/lib/insika/fence.rb +96 -0
  90. data/lib/insika/golden_store.rb +3 -0
  91. data/lib/insika/harvest.rb +26 -14
  92. data/lib/insika/harvest_engine.rb +1 -0
  93. data/lib/insika/harvest_store.rb +12 -0
  94. data/lib/insika/http_client.rb +87 -4
  95. data/lib/insika/kits.rb +54 -0
  96. data/lib/insika/knowledge.rb +34 -20
  97. data/lib/insika/llm_configurator.rb +46 -4
  98. data/lib/insika/llm_provider_store.rb +1 -3
  99. data/lib/insika/llm_trace_store.rb +54 -0
  100. data/lib/insika/mcp_client.rb +27 -63
  101. data/lib/insika/mcp_live_tool.rb +55 -9
  102. data/lib/insika/mcp_store.rb +37 -3
  103. data/lib/insika/mcp_tool_registry.rb +76 -26
  104. data/lib/insika/media.rb +38 -25
  105. data/lib/insika/memory_store.rb +16 -3
  106. data/lib/insika/memory_trim.rb +60 -0
  107. data/lib/insika/middleware.rb +12 -3
  108. data/lib/insika/model_metrics_store.rb +285 -0
  109. data/lib/insika/model_registry.rb +110 -0
  110. data/lib/insika/model_resolver.rb +1 -1
  111. data/lib/insika/model_selection.rb +14 -21
  112. data/lib/insika/model_visible.rb +17 -6
  113. data/lib/insika/onboarding.rb +1 -0
  114. data/lib/insika/outcome_store.rb +1 -1
  115. data/lib/insika/overlay_tool_registry.rb +30 -2
  116. data/lib/insika/pack_importer.rb +54 -9
  117. data/lib/insika/packaging.rb +14 -5
  118. data/lib/insika/pending_action_store.rb +45 -18
  119. data/lib/insika/plugin/loader.rb +4 -2
  120. data/lib/insika/prefix_fingerprint.rb +32 -27
  121. data/lib/insika/profile_source.rb +5 -0
  122. data/lib/insika/provider_error_classifier.rb +18 -3
  123. data/lib/insika/queue_policy.rb +7 -2
  124. data/lib/insika/refinement/proposer.rb +42 -17
  125. data/lib/insika/reliability.rb +12 -3
  126. data/lib/insika/router/app.rb +14 -5
  127. data/lib/insika/router/session_key.rb +35 -5
  128. data/lib/insika/safety/config.rb +20 -3
  129. data/lib/insika/safety/corpus.rb +10 -3
  130. data/lib/insika/safety/factory.rb +39 -1
  131. data/lib/insika/safety/input_guardrail.rb +21 -1
  132. data/lib/insika/safety/judge_moderator.rb +70 -0
  133. data/lib/insika/safety/output_validator.rb +29 -5
  134. data/lib/insika/safety/prompt_echo.rb +89 -0
  135. data/lib/insika/schema_guard.rb +53 -76
  136. data/lib/insika/server/app.rb +58 -7
  137. data/lib/insika/server/rack_app.rb +2 -0
  138. data/lib/insika/server/responses.rb +50 -5
  139. data/lib/insika/session_actor.rb +58 -28
  140. data/lib/insika/session_store.rb +163 -11
  141. data/lib/insika/settings_store.rb +57 -2
  142. data/lib/insika/shared_conversations.rb +305 -0
  143. data/lib/insika/skill_catalog.rb +28 -2
  144. data/lib/insika/skill_store.rb +6 -0
  145. data/lib/insika/soak/runner.rb +1 -1
  146. data/lib/insika/spoken_transcript.rb +31 -0
  147. data/lib/insika/steer_injector.rb +7 -48
  148. data/lib/insika/store.rb +29 -1
  149. data/lib/insika/stores/json_value.rb +37 -0
  150. data/lib/insika/stores/memory.rb +22 -35
  151. data/lib/insika/stores/postgres.rb +295 -0
  152. data/lib/insika/stores/sqlite.rb +257 -43
  153. data/lib/insika/stores/turn_counter.rb +25 -0
  154. data/lib/insika/studio/app.rb +1459 -163
  155. data/lib/insika/studio/assets/dist/application.css +1 -1
  156. data/lib/insika/studio/assets/dist/application.js +25 -25
  157. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  158. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  159. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  160. data/lib/insika/studio/assets/dist/fonts/ibm-plex-sans-latin-wght-normal.woff2 +0 -0
  161. data/lib/insika/studio/forms.rb +189 -12
  162. data/lib/insika/studio/nav_icons.rb +3 -0
  163. data/lib/insika/studio/views/_agent_config_retrieval.erb +73 -0
  164. data/lib/insika/studio/views/_agent_picker.erb +15 -0
  165. data/lib/insika/studio/views/_agent_tab_config.erb +83 -243
  166. data/lib/insika/studio/views/_agent_tab_history.erb +56 -1
  167. data/lib/insika/studio/views/_agent_tab_performance.erb +34 -0
  168. data/lib/insika/studio/views/_agent_tab_prompts.erb +7 -9
  169. data/lib/insika/studio/views/_agent_tab_skills.erb +16 -7
  170. data/lib/insika/studio/views/_agent_tab_tools.erb +90 -0
  171. data/lib/insika/studio/views/_agents_master.erb +8 -12
  172. data/lib/insika/studio/views/_cfg_alerts.erb +4 -0
  173. data/lib/insika/studio/views/_cfg_budget.erb +20 -0
  174. data/lib/insika/studio/views/_cfg_distill.erb +26 -0
  175. data/lib/insika/studio/views/_cfg_followup.erb +30 -0
  176. data/lib/insika/studio/views/_cfg_grounding.erb +21 -0
  177. data/lib/insika/studio/views/_cfg_guardrails.erb +47 -0
  178. data/lib/insika/studio/views/_cfg_harvest.erb +35 -0
  179. data/lib/insika/studio/views/_cfg_reliability.erb +32 -0
  180. data/lib/insika/studio/views/_chats_master.erb +66 -0
  181. data/lib/insika/studio/views/_customers_master.erb +37 -0
  182. data/lib/insika/studio/views/_empty.erb +15 -0
  183. data/lib/insika/studio/views/_facts_master.erb +42 -0
  184. data/lib/insika/studio/views/_harvest_candidate_detail.erb +14 -0
  185. data/lib/insika/studio/views/_identity.erb +16 -0
  186. data/lib/insika/studio/views/_kpi.erb +17 -0
  187. data/lib/insika/studio/views/_message.erb +40 -2
  188. data/lib/insika/studio/views/_model_chart.erb +82 -0
  189. data/lib/insika/studio/views/_refinement_master.erb +38 -0
  190. data/lib/insika/studio/views/{tool_edit.erb → _tool_edit.erb} +38 -11
  191. data/lib/insika/studio/views/_tool_users.erb +17 -0
  192. data/lib/insika/studio/views/_why.erb +53 -0
  193. data/lib/insika/studio/views/agent_detail.erb +17 -10
  194. data/lib/insika/studio/views/agents.erb +28 -16
  195. data/lib/insika/studio/views/approvals.erb +16 -11
  196. data/lib/insika/studio/views/artifact.erb +20 -7
  197. data/lib/insika/studio/views/artifacts.erb +19 -14
  198. data/lib/insika/studio/views/chats.erb +22 -29
  199. data/lib/insika/studio/views/customer.erb +88 -43
  200. data/lib/insika/studio/views/customers.erb +16 -26
  201. data/lib/insika/studio/views/evals.erb +49 -31
  202. data/lib/insika/studio/views/fact_detail.erb +114 -0
  203. data/lib/insika/studio/views/facts.erb +15 -116
  204. data/lib/insika/studio/views/followups.erb +63 -52
  205. data/lib/insika/studio/views/funnel.erb +104 -43
  206. data/lib/insika/studio/views/harvest.erb +85 -59
  207. data/lib/insika/studio/views/home.erb +138 -23
  208. data/lib/insika/studio/views/inbox.erb +36 -0
  209. data/lib/insika/studio/views/kits.erb +176 -0
  210. data/lib/insika/studio/views/knowledge.erb +92 -80
  211. data/lib/insika/studio/views/layout.erb +42 -11
  212. data/lib/insika/studio/views/live.erb +45 -0
  213. data/lib/insika/studio/views/mcp.erb +43 -13
  214. data/lib/insika/studio/views/models.erb +187 -0
  215. data/lib/insika/studio/views/parity.erb +38 -32
  216. data/lib/insika/studio/views/playground.erb +13 -9
  217. data/lib/insika/studio/views/refinement.erb +15 -224
  218. data/lib/insika/studio/views/refinement_detail.erb +243 -0
  219. data/lib/insika/studio/views/session.erb +153 -118
  220. data/lib/insika/studio/views/settings.erb +86 -22
  221. data/lib/insika/studio/views/skills.erb +53 -19
  222. data/lib/insika/studio/views/system_files.erb +7 -7
  223. data/lib/insika/studio/views/task.erb +45 -15
  224. data/lib/insika/studio/views/tasks.erb +35 -18
  225. data/lib/insika/studio/views/tools.erb +62 -73
  226. data/lib/insika/task_actor.rb +4 -1
  227. data/lib/insika/task_store.rb +67 -1
  228. data/lib/insika/telemetry/pricing.rb +16 -14
  229. data/lib/insika/telemetry/recorder.rb +115 -9
  230. data/lib/insika/telemetry/ruby_llm_instrumenter.rb +110 -0
  231. data/lib/insika/telemetry.rb +12 -2
  232. data/lib/insika/templates/daily-digest/README.md +13 -0
  233. data/lib/insika/templates/daily-digest/agent.rb +22 -11
  234. data/lib/insika/testing/store_contract.rb +65 -0
  235. data/lib/insika/text_tool_call.rb +66 -0
  236. data/lib/insika/tick.rb +7 -1
  237. data/lib/insika/token_store.rb +5 -0
  238. data/lib/insika/tool_assembly.rb +21 -13
  239. data/lib/insika/tool_batch.rb +1 -1
  240. data/lib/insika/tool_catalog.rb +12 -2
  241. data/lib/insika/tool_definition.rb +82 -13
  242. data/lib/insika/tool_envelope.rb +266 -29
  243. data/lib/insika/tool_manifest.rb +1 -0
  244. data/lib/insika/tool_store.rb +12 -4
  245. data/lib/insika/tool_trace_store.rb +15 -8
  246. data/lib/insika/tool_usage_report.rb +14 -4
  247. data/lib/insika/tools/a2a_remote.rb +1 -1
  248. data/lib/insika/tools/agent_enum.rb +1 -1
  249. data/lib/insika/tools/confirm_pending.rb +112 -0
  250. data/lib/insika/tools/data_defined_tool.rb +24 -15
  251. data/lib/insika/tools/generate_image.rb +2 -2
  252. data/lib/insika/tools/load_knowledge.rb +7 -3
  253. data/lib/insika/tools/load_skill.rb +3 -3
  254. data/lib/insika/tools/present.rb +115 -0
  255. data/lib/insika/tools/remember.rb +7 -2
  256. data/lib/insika/tools/run_persona_eval.rb +9 -6
  257. data/lib/insika/tools/save_artifact.rb +16 -6
  258. data/lib/insika/tools/schedule_followup.rb +3 -3
  259. data/lib/insika/tools/stuck_signal.rb +3 -3
  260. data/lib/insika/tools/subagent.rb +5 -5
  261. data/lib/insika/tools/subagents.rb +3 -3
  262. data/lib/insika/tools/tool_search.rb +11 -7
  263. data/lib/insika/tools/tts.rb +2 -2
  264. data/lib/insika/tools/update_briefing.rb +4 -4
  265. data/lib/insika/turn_output.rb +2 -2
  266. data/lib/insika/turn_state.rb +31 -1
  267. data/lib/insika/turn_timing.rb +51 -14
  268. data/lib/insika/version.rb +1 -1
  269. data/lib/insika/wiring/graph.rb +88 -16
  270. data/lib/insika/wiring/graph_chat.rb +4 -0
  271. data/lib/insika/workflow.rb +25 -64
  272. data/lib/insika.rb +23 -3
  273. metadata +74 -13
  274. data/lib/insika/commands/import_mcp_tools.rb +0 -48
  275. data/lib/insika/mcp_http_client.rb +0 -67
  276. data/lib/insika/mcp_tool_ingestor.rb +0 -165
@@ -1,12 +1,13 @@
1
- <%# Overview home: stat cards + activity chart + recent
1
+ <%# Overview home: KPI strip + activity chart + recent
2
2
  conversations, all LIVE over the existing SSE channel (live_home_controller
3
3
  subscribes to /studio/events; the server-rendered numbers are its baseline).
4
4
  Charts are inline SVG (geometry via attributes, fill via stylesheet —
5
5
  CSP-safe, no inline style). %>
6
- <div data-controller="live-home" data-live-home-active-value="<%= @active_now %>">
6
+ <div class="reports-page reports-home" data-controller="live-home" data-live-home-active-value="<%= @active_now %>">
7
7
  <header class="page-head">
8
8
  <div>
9
- <h1>Overview<span class="sub">at a glance</span></h1>
9
+ <%== crumbs_for("Overview") %>
10
+ <h1>Overview<span class="sub">Conversations & activity</span></h1>
10
11
  </div>
11
12
  <div class="head-actions">
12
13
  <%== agent_filter_form("/studio/home", @agent) %>
@@ -18,35 +19,56 @@
18
19
  <b class="tnum" data-live-home-target="count"><%= @active_now %></b> active now
19
20
  <span class="live-ago muted" data-live-home-target="ago"></span>
20
21
  </span>
21
- <a class="btn primary" href="/studio/playground">Open playground →</a>
22
+ <a class="btn primary" href="/studio/playground">Open playground</a>
22
23
  </div>
23
24
  </header>
24
25
 
25
- <div class="stat-cards">
26
- <% [["conversations", "Conversations", @conv_trend],
27
- ["messages", "Messages", @msg_trend],
28
- ["agents", "Agents", nil],
29
- ["skills", "Skills", nil],
30
- ["tools", "Tools", nil],
31
- ["providers", "LLM providers", nil]].each do |key, label, trend| %>
32
- <div class="stat-card">
33
- <div class="stat-num tnum"><%= @counts[key] %></div>
34
- <div class="stat-label"><%= label %><% if trend && !trend.zero? %>
35
- <span class="trend <%= trend.positive? ? "up" : "down" %>" title="vs yesterday"><%= trend.positive? ? "+" : "−" %><%= trend.abs %></span>
36
- <% end %></div>
37
- </div>
26
+ <%# home-ops: everything below the header. live-home reloads this frame
27
+ (debounced) when a turn ends, so the numbers follow the traffic. %>
28
+ <turbo-frame id="home-ops" target="_top" data-live-home-target="ops">
29
+ <% totals = @day && @day["totals"] %>
30
+ <% turns = @day && @day["turns"] %>
31
+ <% p95 = turns && turns["windows"]["total_ms"]["p95"] %>
32
+ <% cost = totals && totals["cost"] %>
33
+ <div class="kpi-strip">
34
+ <% window = "last 14 days" %>
35
+ <% [["Conversations", @counts["conversations"], @conv_trend, (window if @agent)],
36
+ ["Messages", @counts["messages"], @msg_trend, window],
37
+ ["Outcomes", @outcomes_24h || "—", nil, "last 24h"],
38
+ ["Cost", usd(cost), nil, @day_before_cost.nil? ? "last 24h" : "last 24h · #{usd(@day_before_cost)} before"],
39
+ ["Turn p95", p95.nil? ? "—" : format("%.1fs", p95 / 1000.0), nil, turns ? "#{turns['count']} turns · 24h" : "last 24h"],
40
+ ["Failed turns", turns && turns["count"].positive? ? format("%.1f%%", turns["failures"] * 100.0 / turns["count"]) : "—", nil, turns ? "#{turns['failures']} in 24h" : "last 24h"]].each do |label, value, trend, sub| %>
41
+ <% delta = (trend && !trend.zero?) ? "#{trend.positive? ? '+' : '-'}#{trend.abs}" : nil %>
42
+ <%== render("_kpi", locals: { label: label, value: value, delta: delta, sub: sub }) %>
38
43
  <% end %>
39
44
  </div>
40
45
 
46
+ <p class="report-note">Every number follows the agent filter. Conversations and messages compare today with yesterday; cost, outcomes and turns cover the last 24 hours. <a href="/studio/models<%= "?agent=#{Rack::Utils.escape(@agent)}" if @agent %>">Models</a> has the detail.</p>
47
+
48
+ <%# What a person should act on now: review queues, stalled customers,
49
+ failing tools. Only non-zero items show. %>
50
+ <% attention = attention_items(turns) %>
51
+ <section class="attention" aria-label="Needs attention">
52
+ <h2>Needs attention</h2>
53
+ <% if attention.empty? %>
54
+ <span class="muted">Nothing waiting on you.</span>
55
+ <% else %>
56
+ <% attention.each do |text, href, tone| %>
57
+ <a class="attention-item <%= tone %>" href="<%= href %>" data-turbo-frame="_top"><%= text %></a>
58
+ <% end %>
59
+ <% end %>
60
+ </section>
61
+
41
62
  <div class="home-grid">
42
63
  <section class="card home-activity">
43
64
  <div class="section-head">
44
65
  <h2>Activity</h2>
45
66
  <span class="muted">conversations touched · last 14 days</span>
46
67
  </div>
47
- <%# 24h sparkline: area-fill + 1.5px line over the hourly
48
- series; same session scan, finer buckets. Geometry via attributes,
49
- colors via classes — CSP-safe like the barchart below. %>
68
+ <%# 24h sparkline: area-fill + 1.5px line over the hourly series, on the
69
+ shared .chart base (Task 1) — same hairline axis and sans labels as
70
+ the funnel's bar chart, just a line/time-series shape instead of
71
+ bars. Geometry via attributes, colors via classes — CSP-safe. %>
50
72
  <% s_max = [@activity_24h.map(&:last).max || 0, 1].max %>
51
73
  <% s_n = @activity_24h.size %>
52
74
  <% s_pts = @activity_24h.each_with_index.map do |(_t, c), i|
@@ -57,11 +79,16 @@
57
79
  <div class="sparkline-meta">
58
80
  <b class="tnum"><%= @activity_24h.sum { |_t, c| c } %></b> conversations · last 24h
59
81
  </div>
60
- <svg class="sparkline" viewBox="0 0 480 48" preserveAspectRatio="none" role="img"
82
+ <svg class="chart chart-sparkline" viewBox="0 0 480 48" preserveAspectRatio="none" role="img"
61
83
  aria-label="conversations per hour, last 24 hours">
62
84
  <polygon class="spark-area" points="<%= s_line %> 480,48 0,48"/>
63
85
  <polyline class="spark-line" points="<%= s_line %>" vector-effect="non-scaling-stroke"/>
86
+ <line class="axis" x1="0" y1="44" x2="480" y2="44" />
64
87
  </svg>
88
+ <div class="chart-labels">
89
+ <span><%= @activity_24h.first&.first&.strftime('%-l%P') %></span>
90
+ <span>now</span>
91
+ </div>
65
92
  </div>
66
93
  <% max = [@activity.map(&:last).max || 0, 1].max %>
67
94
  <svg class="barchart" viewBox="0 0 700 176" preserveAspectRatio="none" role="img" aria-label="conversations per day, last 14 days">
@@ -85,7 +112,7 @@
85
112
  <section class="card home-recent">
86
113
  <div class="section-head">
87
114
  <h2>Recent conversations</h2>
88
- <a class="muted" href="/studio/chats">all →</a>
115
+ <a class="muted" href="/studio/chats<%= "?agent=#{Rack::Utils.escape(@agent)}" if @agent %>">See all</a>
89
116
  </div>
90
117
  <% cutoff = Time.now - (5 * 60) %>
91
118
  <ul class="convo-rail" data-live-home-target="list">
@@ -98,7 +125,7 @@
98
125
  <a href="/studio/sessions/<%= Rack::Utils.escape(s.id) %>">
99
126
  <span class="avatar sm <%= avatar_class(s.id) %>"><%= s.id[0, 2].upcase %></span>
100
127
  <span class="convo-main">
101
- <span class="convo-id mono"><%= s.id[0, 20] %></span>
128
+ <span class="convo-id mono" title="<%= s.id %>"><%= s.id[0, 20] %></span>
102
129
  <span class="convo-sub"><%= Array(s.messages).size %> msg · <%= time_ago(s.updated_at) %></span>
103
130
  </span>
104
131
  <span class="presence<%= " present" if fresh %>" title="touched in the last 5 minutes"></span>
@@ -109,4 +136,92 @@
109
136
  </ul>
110
137
  </section>
111
138
  </div>
139
+
140
+ <div class="home-grid">
141
+ <section class="card">
142
+ <div class="section-head"><h2>Cost per day</h2><span class="muted">reported USD · tokens in the tooltip · UTC days</span></div>
143
+ <% d_costs = @daily.map { |b| b["cost"] || 0 } %>
144
+ <% if @daily.empty? || d_costs.sum.zero? %>
145
+ <div class="empty">No reported cost in the last 14 days.</div>
146
+ <% else %>
147
+ <% d_max = [d_costs.max, 0.000001].max %>
148
+ <svg class="barchart" viewBox="0 0 700 176" preserveAspectRatio="none" role="img" aria-label="reported cost per day, last 14 days">
149
+ <% n = @daily.size; slot = 700.0 / n; bw = slot * 0.58 %>
150
+ <% @daily.each_with_index do |b, i| %>
151
+ <% c = b["cost"] || 0; h = (c / d_max) * 150.0; x = (i * slot) + ((slot - bw) / 2.0) %>
152
+ <% tokens = %w[input_tokens output_tokens].sum { |k| b[k].to_i } %>
153
+ <rect class="bar<%== ' bar-empty' if c.zero? %>" x="<%= format('%.1f', x) %>" y="<%= format('%.1f', c.zero? ? 157.0 : 160.0 - h) %>"
154
+ width="<%= format('%.1f', bw) %>" height="<%= format('%.1f', c.zero? ? 3.0 : [h, 3.0].max) %>" rx="2">
155
+ <title><%= Time.iso8601(b["at"]).utc.strftime('%b %-d') %> · <%= usd(c) %> · <%= b["requests"] %> requests · <%= tokens %> tokens</title>
156
+ </rect>
157
+ <% end %>
158
+ <line class="barchart-axis-line" x1="0" y1="160" x2="700" y2="160" />
159
+ </svg>
160
+ <div class="barchart-axis">
161
+ <span><%= Time.iso8601(@daily.first["at"]).utc.strftime('%b %-d') %></span>
162
+ <span class="peak"><%= usd(d_costs.sum) %> in 14 days · peak <%= usd(d_max) %>/day</span>
163
+ <span><%= Time.iso8601(@daily.last["at"]).utc.strftime('%b %-d') %></span>
164
+ </div>
165
+ <% end %>
166
+ </section>
167
+
168
+ <section class="card">
169
+ <div class="section-head"><h2>Waiting for a reply</h2><span class="muted">customer spoke last, 10+ min ago · 24h</span></div>
170
+ <% if @stalled.empty? %>
171
+ <div class="empty">No conversation is waiting.</div>
172
+ <% else %>
173
+ <table class="grid">
174
+ <thead><tr><th>conversation</th><th>agent</th><th class="num">waiting</th></tr></thead>
175
+ <tbody>
176
+ <% @stalled.each do |row| %>
177
+ <tr><td><a class="mono" href="/studio/sessions/<%= Rack::Utils.escape(row[:id]) %>" title="<%= row[:id] %>"><%= row[:id][0, 20] %></a></td>
178
+ <td><%= row[:agent].empty? ? "—" : row[:agent] %></td><td class="num"><%= time_ago(row[:waiting].iso8601) %></td></tr>
179
+ <% end %>
180
+ </tbody>
181
+ </table>
182
+ <% end %>
183
+ </section>
184
+ </div>
185
+
186
+ <div class="home-grid">
187
+ <section class="card">
188
+ <div class="section-head"><h2>Agents</h2><span class="muted">conversations · last 14 days</span></div>
189
+ <% if @agent_ranking.empty? %>
190
+ <div class="empty">No agent conversations yet.</div>
191
+ <% else %>
192
+ <table class="grid">
193
+ <thead><tr><th>agent</th><th class="num">today</th><th class="num">14 days</th><th class="num">messages</th></tr></thead>
194
+ <tbody>
195
+ <% @agent_ranking.each do |row| %>
196
+ <tr><td><a href="/studio/chats?agent=<%= Rack::Utils.escape(row[:agent]) %>"><%= row[:agent] %></a></td>
197
+ <td class="num"><%= row[:today] %></td><td class="num"><%= row[:conversations] %></td><td class="num"><%= row[:messages] %></td></tr>
198
+ <% end %>
199
+ </tbody>
200
+ </table>
201
+ <% end %>
202
+ <% unless @cold_agents.empty? %>
203
+ <p class="muted">No conversation in <%= COLD_AGENT_DAYS %>+ days: <%== @cold_agents.map { |id| %(<a href="/studio/agents/#{Rack::Utils.escape(id)}">#{Rack::Utils.escape_html(id)}</a>) }.join(", ") %></p>
204
+ <% end %>
205
+ </section>
206
+
207
+ <section class="card">
208
+ <div class="section-head"><h2>Tool health</h2><span class="muted">last 24h</span></div>
209
+ <% tools = turns ? turns["tools"].sort_by { |t| [-t["failures"], -t["calls"]] }.first(8) : [] %>
210
+ <% if tools.empty? %>
211
+ <div class="empty">No tool calls in the last 24h.</div>
212
+ <% else %>
213
+ <table class="grid">
214
+ <thead><tr><th>tool</th><th class="num">calls</th><th class="num">ok %</th><th class="num">p95 ms</th></tr></thead>
215
+ <tbody>
216
+ <% tools.each do |t| %>
217
+ <tr><td><%= t["tool"] %></td><td class="num"><%= t["calls"] %></td>
218
+ <td class="num"><%= format("%.1f", (t["calls"] - t["failures"]) * 100.0 / t["calls"]) %></td>
219
+ <td class="num"><%= t["p95_ms"] || "—" %></td></tr>
220
+ <% end %>
221
+ </tbody>
222
+ </table>
223
+ <% end %>
224
+ </section>
225
+ </div>
226
+ </turbo-frame>
112
227
  </div>
@@ -0,0 +1,36 @@
1
+ <%# Review inbox — every item parked on a person, across Approvals, Facts,
2
+ Harvest, Refinement and guardrail false-positive reports. Oldest first;
3
+ each row opens the page where the decision is made. %>
4
+ <header class="page-head">
5
+ <div>
6
+ <%== crumbs_for("Inbox") %>
7
+ <h1>Inbox<span class="sub"><%= @inbox_total %> waiting on a person</span></h1>
8
+ </div>
9
+ </header>
10
+
11
+ <nav class="segmented inbox-tabs" aria-label="Queue">
12
+ <a href="/studio/inbox"<%== ' aria-current="page"' unless @inbox_type %>>All&nbsp;<span class="count"><%= @inbox_total %></span></a>
13
+ <% Studio::App::INBOX_TYPES.each do |type, label| %>
14
+ <a href="/studio/inbox?type=<%= type %>"<%== ' aria-current="page"' if @inbox_type == type %>><%= label %>&nbsp;<span class="count"><%= @inbox_counts[type] %></span></a>
15
+ <% end %>
16
+ </nav>
17
+
18
+ <% if @items.empty? %>
19
+ <%== render("_empty", locals: { icon: :approvals, text: "Nothing waiting on you." }) %>
20
+ <% else %>
21
+ <div class="card">
22
+ <table class="grid">
23
+ <thead><tr><th>type</th><th>item</th><th>agent</th><th>since</th></tr></thead>
24
+ <tbody>
25
+ <% @items.each do |i| %>
26
+ <tr>
27
+ <td><span class="inbox-type <%= i[:type] %>"><%= Studio::App::INBOX_TYPES[i[:type]] %></span></td>
28
+ <td><a href="<%= i[:href] %>"><%= i[:title] %></a></td>
29
+ <td class="mono"><%= i[:agent] %></td>
30
+ <td class="mono"><%= i[:at] ? time_ago(i[:at]) : "—" %></td>
31
+ </tr>
32
+ <% end %>
33
+ </tbody>
34
+ </table>
35
+ </div>
36
+ <% end %>
@@ -0,0 +1,176 @@
1
+ <%# Kits — drill-down: master list | kit editor + membership. A kit bundles
2
+ skills, tools and MCP servers; agents that list it (AgentProfile#kits) get
3
+ them at read time (Insika::Kits). @selected nil = none open, "" = new. %>
4
+ <header class="page-head">
5
+ <h1>Kits<span class="sub">Bundles of skills and tools that agents share</span></h1>
6
+ </header>
7
+ <div class="drill" data-controller="list-filter">
8
+ <aside class="drill-pane drill-master">
9
+ <div class="drill-pane-head">
10
+ <div class="title"><strong>Kits <span class="count"><%= @kits.size %></span></strong></div>
11
+ <a class="btn primary btn-sm" href="/studio/kits/new">+ new</a>
12
+ </div>
13
+ <div class="drill-search">
14
+ <input type="search" class="filter-input" placeholder="Search kits…" aria-label="Filter kits"
15
+ data-list-filter-target="query" data-action="input->list-filter#filter keydown->list-filter#clear">
16
+ </div>
17
+ <nav class="drill-pane-body drill-list" aria-label="kits">
18
+ <% if @kits.empty? %>
19
+ <div class="empty">No kits yet. Create one to share skills and tools across agents.</div>
20
+ <% end %>
21
+ <% @kits.each do |name, kit| %>
22
+ <% members = @agents.count { |a| Array(a.kits).include?(name) } %>
23
+ <a class="drill-item<%== ' active' if @selected == name %>"
24
+ href="/studio/kits/<%= Rack::Utils.escape(name) %>"
25
+ data-list-filter-target="item" data-filter-text="<%= name %> <%= kit["description"] %>">
26
+ <div class="drill-item-name"><span class="txt" title="<%= name %>"><%= name %></span><span class="pill plain"><%= members %> <%= members == 1 ? "agent" : "agents" %></span></div>
27
+ <div class="drill-item-sub"><%= kit["description"] %></div>
28
+ </a>
29
+ <% end %>
30
+ <div class="empty" data-list-filter-target="empty" hidden>No kits match your filter.</div>
31
+ </nav>
32
+ </aside>
33
+
34
+ <section class="drill-pane drill-detail">
35
+ <% if @selected.nil? %>
36
+ <div class="drill-empty">
37
+ <div>
38
+ <h2>Kits</h2>
39
+ <p>Pick a kit, or create one. A kit bundles skills and tools; every agent in it gets them on its next turn.</p>
40
+ </div>
41
+ </div>
42
+ <% else %>
43
+ <% new = @selected == "" %>
44
+ <% kit = @kit || {} %>
45
+ <%# write_kit replaces the whole kit: a stored item the live catalog does not list
46
+ (an MCP server down, a non-MCP group) is rendered checked so a save keeps it. %>
47
+ <% gone_skills = Array(kit["skills"]) - @skills.map(&:name) %>
48
+ <% gone_tools = Array(kit["tools"]) - @tools.map(&:name) %>
49
+ <% gone_groups = Array(kit["tool_groups"]) - @tool_group_names %>
50
+ <div class="drill-pane-head">
51
+ <div class="title"><strong class="mono"><%= new ? "New kit" : @selected %></strong></div>
52
+ <button type="submit" form="kit-form" class="btn primary" data-turbo-submits-with="Saving…">Save kit</button>
53
+ </div>
54
+ <div class="drill-pane-body">
55
+ <form id="kit-form" method="post" action="/studio/kits" data-controller="dirty-guard">
56
+ <%== csrf_tag %>
57
+ <% if new %>
58
+ <label>name <span class="muted">(lowercase slug, e.g. grocery)</span>
59
+ <input type="text" name="name" placeholder="grocery" required pattern="[a-z0-9][a-z0-9_\-]{0,63}">
60
+ </label>
61
+ <% else %>
62
+ <input type="hidden" name="name" value="<%= @selected %>">
63
+ <% end %>
64
+ <label>description
65
+ <input type="text" name="description" value="<%= kit["description"] %>" placeholder="What agents in this kit have in common">
66
+ </label>
67
+
68
+ <details class="tool-section" open>
69
+ <summary>Skills <span class="count"><%= Array(kit["skills"]).size %>/<%= @skills.size %></span></summary>
70
+ <div class="check-grid tool-grid">
71
+ <% @skills.each do |s| %>
72
+ <label class="check tool-check" title="<%= s.description %>">
73
+ <input type="checkbox" name="skills[]" value="<%= s.name %>"<%== " checked" if Array(kit["skills"]).include?(s.name) %>>
74
+ <span class="mono"><%= s.name %></span>
75
+ </label>
76
+ <% end %>
77
+ <% gone_skills.each do |name| %>
78
+ <label class="check tool-check" title="stored in the kit, not in the skill catalog right now">
79
+ <input type="checkbox" name="skills[]" value="<%= name %>" checked>
80
+ <span class="mono"><%= name %></span> <span class="pill warn">not in catalog</span>
81
+ </label>
82
+ <% end %>
83
+ </div>
84
+ </details>
85
+
86
+ <% if @tool_group_names.any? || gone_groups.any? %>
87
+ <details class="tool-section" open>
88
+ <summary>MCP servers <span class="count"><%= Array(kit["tool_groups"]).size %>/<%= @tool_group_names.size %></span></summary>
89
+ <div class="check-grid tool-grid">
90
+ <% @tool_group_names.each do |g| %>
91
+ <label class="check tool-check">
92
+ <input type="checkbox" name="tool_groups[]" value="<%= g %>"<%== " checked" if Array(kit["tool_groups"]).include?(g) %>>
93
+ <span class="mono">all tools from <%= g.delete_prefix("mcp:") %></span>
94
+ </label>
95
+ <% end %>
96
+ <% gone_groups.each do |g| %>
97
+ <label class="check tool-check" title="stored in the kit, no live tool carries this group right now">
98
+ <input type="checkbox" name="tool_groups[]" value="<%= g %>" checked>
99
+ <span class="mono"><%= g %></span> <span class="pill warn">not in catalog</span>
100
+ </label>
101
+ <% end %>
102
+ </div>
103
+ </details>
104
+ <% end %>
105
+
106
+ <div class="only-on-scope" data-controller="list-filter">
107
+ <details class="tool-section" open>
108
+ <summary>Tools <span class="count"><%= Array(kit["tools"]).size %>/<%= @tools.size %></span></summary>
109
+ <div class="matrix-topline">
110
+ <label class="switch" title="hide tools not in this kit; typing a search shows every match">
111
+ <input type="checkbox" id="only-on" checked>
112
+ <span class="switch-track" aria-hidden="true"></span>
113
+ <span class="switch-label">only in kit</span>
114
+ </label>
115
+ </div>
116
+ <input type="search" class="filter-input" placeholder="Search to add tools…" aria-label="Search tools"
117
+ data-list-filter-target="query" data-action="input->list-filter#filter keydown->list-filter#clear">
118
+ <p class="empty" data-list-filter-target="empty" hidden>No tools match your filter.</p>
119
+ <div class="check-grid tool-grid">
120
+ <% @tools.each do |t| %>
121
+ <label class="check tool-check" title="<%= t.description %>"
122
+ data-list-filter-target="item" data-filter-text="<%= t.name %>">
123
+ <input type="checkbox" name="tools[]" value="<%= t.name %>"<%== " checked" if Array(kit["tools"]).include?(t.name) %>>
124
+ <span class="mono"><%= t.name %></span>
125
+ </label>
126
+ <% end %>
127
+ <% gone_tools.each do |name| %>
128
+ <label class="check tool-check" title="stored in the kit, not in the tool catalog right now"
129
+ data-list-filter-target="item" data-filter-text="<%= name %>">
130
+ <input type="checkbox" name="tools[]" value="<%= name %>" checked>
131
+ <span class="mono"><%= name %></span> <span class="pill warn">not in catalog</span>
132
+ </label>
133
+ <% end %>
134
+ </div>
135
+ </details>
136
+ </div>
137
+ </form>
138
+
139
+ <% unless new %>
140
+ <div class="drill-section">
141
+ <div class="drill-section-head">
142
+ <h3>Agents</h3>
143
+ <span class="muted">Checked agents get this kit's skills and tools on their next turn. An agent allowed every tool or every skill keeps that.</span>
144
+ </div>
145
+ <form method="post" action="/studio/kits/<%= Rack::Utils.escape(@selected) %>/agents" data-controller="dirty-guard">
146
+ <%== csrf_tag %>
147
+ <% if @agents.empty? %>
148
+ <p class="muted">No agents configured.</p>
149
+ <% else %>
150
+ <div class="check-grid tool-grid">
151
+ <% @agents.each do |a| %>
152
+ <label class="check tool-check">
153
+ <input type="checkbox" name="agent_ids[]" value="<%= a.id %>"<%== " checked" if Array(a.kits).include?(@selected) %>>
154
+ <span class="mono"><%= a.id %></span>
155
+ <% if (a.tools_allow.nil? && a.tools_allow_groups.nil?) || a.skills.nil? %>
156
+ <span class="pill warn" title="this agent gets every tool or skill, so the kit adds nothing there — limit it in Tools or in its Skills tab">all</span>
157
+ <% end %>
158
+ </label>
159
+ <% end %>
160
+ </div>
161
+ <div class="actions"><button type="submit" class="primary" data-turbo-submits-with="Applying…">Apply to agents</button></div>
162
+ <% end %>
163
+ </form>
164
+ </div>
165
+
166
+ <div class="drill-section">
167
+ <form method="post" action="/studio/kits/<%= Rack::Utils.escape(@selected) %>/delete">
168
+ <%== csrf_tag %>
169
+ <div class="actions"><button type="submit" data-turbo-confirm="Remove kit? Agents in it lose its skills and tools.">Delete kit</button></div>
170
+ </form>
171
+ </div>
172
+ <% end %>
173
+ </div>
174
+ <% end %>
175
+ </section>
176
+ </div>