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,23 +1,36 @@
1
+ <div class="reports-page reports-artifact">
1
2
  <%# the artifact preview page. The content renders ONLY inside a SANDBOXED
2
3
  iframe (no scripts, no same-origin, no forms) on top of the content
3
4
  route's own restrictive CSP — artifact content is LLM output, untrusted. %>
4
5
  <header class="page-head">
5
6
  <div>
6
- <nav class="crumbs"><a href="/studio/artifacts">Artifacts</a></nav>
7
- <h1><%= @artifact.title %><span class="sub"><span class="mono"><%= @artifact.id %></span> · <%= @artifact.mime %> · <%= @artifact.created_at %></span></h1>
7
+ <nav class="crumbs"><a href="/studio/artifacts?agent=<%= Rack::Utils.escape(@artifact.agent) %>">Artifacts</a></nav>
8
+ <h1><%= @artifact.title %><span class="sub"><%= @artifact.agent %> · <%= time_ago(@artifact.created_at) %></span></h1>
8
9
  </div>
9
10
  <div class="head-actions">
10
11
  <% if @signed_url %>
11
- <button type="button" class="btn sm" data-copy="<%= @signed_url %>">copy signed link</button>
12
+ <button type="button" class="btn btn-sm" data-copy="<%= @signed_url %>">copy signed link</button>
12
13
  <% end %>
13
- <a class="btn sm ghost" href="<%= @content_url %>" target="_blank" rel="noopener">open raw</a>
14
- <form method="post" action="/studio/artifacts/<%= Rack::Utils.escape_path(@artifact.id) %>/delete" class="inline">
14
+ <a class="btn btn-sm ghost" href="<%= @content_url %>" target="_blank" rel="noopener">open raw</a>
15
+ <form method="post" action="/studio/artifacts/<%= Rack::Utils.escape_path(@artifact.id) %>/delete" class="inline" data-turbo-confirm="Delete this artifact?">
15
16
  <%== csrf_tag %>
16
- <button class="btn sm ghost danger">delete</button>
17
+ <input type="hidden" name="agent" value="<%= @artifact.agent %>">
18
+ <button class="btn btn-sm ghost danger">delete</button>
17
19
  </form>
18
20
  </div>
19
21
  </header>
20
22
 
23
+ <details class="report-details">
24
+ <summary>Artifact details</summary>
25
+ <dl class="kv">
26
+ <dt>ID</dt><dd class="mono"><%= @artifact.id %></dd>
27
+ <dt>Format</dt><dd><%= @artifact.mime %></dd>
28
+ <dt>Created</dt><dd><%= @artifact.created_at %></dd>
29
+ <dt>Task</dt><dd><a href="<%= task_path(@artifact.task_id) %>"><%= @artifact.task_id %></a></dd>
30
+ </dl>
31
+ </details>
32
+
21
33
  <div class="card artifact-frame">
22
34
  <iframe sandbox src="<%= @content_url %>" title="<%= @artifact.title %>"></iframe>
23
- </div>
35
+ </div>
36
+ </div>
@@ -1,11 +1,12 @@
1
+ <div class="reports-page reports-artifacts">
1
2
  <%# the Artifacts page: the report destination. Reads the ArtifactStore
2
3
  directly (the listing IS the history); the only mutation — delete —
3
4
  dispatches :delete_artifact on the bus. The preview is a sandboxed iframe
4
5
  on top of the content route's own restrictive CSP. %>
5
6
  <header class="page-head">
6
7
  <div>
7
- <nav class="crumbs"><span>operate</span></nav>
8
- <h1>Artifacts<span class="sub">the report destination — saved pages, per agent</span></h1>
8
+ <%== crumbs_for("Artifacts") %>
9
+ <h1>Artifacts<span class="sub">Saved reports & pages</span></h1>
9
10
  </div>
10
11
  <div class="head-actions">
11
12
  <%== agent_filter_form("/studio/artifacts", @agent) %>
@@ -19,36 +20,38 @@
19
20
  has <span class="mono">save_artifact</span> in its <span class="mono">tools_allow</span>
20
21
  writes its reports here.
21
22
  <% else %>
22
- No artifacts yet, for any agent.
23
+ No artifacts yet. Reports appear here when an agent uses <code>save_artifact</code>.
23
24
  <% end %>
24
25
  </div>
25
26
  <% else %>
26
- <h2><%= @agent || "all agents" %><span class="sub"><%= @artifacts.size %> artifact(s)</span></h2>
27
- <div class="card">
27
+ <div class="section-head"><h2>Saved artifacts</h2><span class="muted"><%= @artifacts.size %> reports · newest first</span></div>
28
+ <div class="card report-table">
28
29
  <table class="grid">
29
30
  <thead>
30
31
  <tr>
31
32
  <% unless @agent %><th>agent</th><% end %>
32
- <th>title</th><th>mime</th><th>task</th><th>created_at</th><th></th>
33
+ <th>Title</th><th>Format</th><th>Task</th><th class="time">Created</th><th><span class="sr-only">Actions</span></th>
33
34
  </tr>
34
35
  </thead>
35
36
  <tbody>
36
37
  <% @artifacts.each do |a| %>
37
38
  <tr>
38
- <% unless @agent %><td class="mono"><%= a.agent %></td><% end %>
39
- <td><a href="/studio/artifacts/<%= Rack::Utils.escape_path(a.id) %>"><%= a.title %></a></td>
40
- <td><span class="pill"><%= a.mime %></span></td>
41
- <td class="mono"><%= a.task_id %></td>
42
- <td class="mono"><%= a.created_at %></td>
39
+ <% unless @agent %>
40
+ <td><%== render("_identity", locals: { id: a.agent, name: a.agent, sub: nil }) %></td>
41
+ <% end %>
42
+ <td class="ellipsis"><a href="/studio/artifacts/<%= Rack::Utils.escape_path(a.id) %>"><%= a.title %></a></td>
43
+ <td><span class="status neutral"><%= a.mime %></span></td>
44
+ <td class="mono"><span title="<%= a.task_id %>" data-copy="<%= a.task_id %>"><%= short_id(a.task_id) %></span></td>
45
+ <td class="time"><span title="<%= a.created_at %>"><%= time_ago(a.created_at) %></span></td>
43
46
  <td class="row-actions">
44
47
  <% if @signed_available %>
45
- <button type="button" class="btn ghost sm" data-copy="<%= signed_artifact_url(a.id) %>">copy link</button>
48
+ <button type="button" class="btn ghost btn-sm" data-copy="<%= signed_artifact_url(a.id) %>">copy link</button>
46
49
  <% end %>
47
50
  <form method="post" action="/studio/artifacts/<%= Rack::Utils.escape_path(a.id) %>/delete"
48
- class="inline" data-confirm="Delete this artifact?">
51
+ class="inline" data-turbo-confirm="Delete this artifact?">
49
52
  <%== csrf_tag %>
50
53
  <input type="hidden" name="agent" value="<%= a.agent %>">
51
- <button class="btn ghost sm danger">delete</button>
54
+ <button class="btn ghost btn-sm danger">delete</button>
52
55
  </form>
53
56
  </td>
54
57
  </tr>
@@ -57,3 +60,5 @@
57
60
  </table>
58
61
  </div>
59
62
  <% end %>
63
+
64
+ </div>
@@ -1,37 +1,30 @@
1
- <header class="page-head">
1
+ <%# Chats — miller columns: master list | detail frame. A row click loads the
2
+ session viewer beside the list (no full page load) and advances the URL,
3
+ same shell as Agents/Customers/Refinement. The detail pane's landing state
4
+ just points at the playground — there is no "create a session" form here,
5
+ conversations are born there, not authored in Studio. %>
6
+ <header class="page-head operation-head">
2
7
  <div>
3
- <nav class="crumbs"><a href="/studio/agents">agents</a> / <span>chats</span></nav>
4
- <h1>Chats<span class="sub"><%= @sessions.size %> recent conversation(s)</span></h1>
8
+ <nav class="crumbs"><a href="/studio/agents">agents</a></nav>
9
+ <h1>Chats<span class="sub"><%= @chats_total || @sessions.size %> conversations</span></h1>
5
10
  </div>
6
11
  <div class="head-actions">
7
- <%== agent_filter_form("/studio/chats", @agent) %>
8
- <a class="btn primary" href="/studio/playground">New conversation →</a>
12
+ <a class="btn primary" href="/studio/playground">New conversation</a>
9
13
  </div>
10
14
  </header>
11
15
 
12
- <% if @sessions.empty? %>
13
- <div class="empty">No conversations yet. Start one in the <a href="/studio/playground">playground</a>.</div>
14
- <% else %>
15
- <div data-controller="list-filter">
16
- <div class="filter-bar">
17
- <input type="search" class="filter-input" placeholder="Filter conversations by id or preview…" aria-label="Filter conversations"
18
- data-list-filter-target="query" data-action="input->list-filter#filter keydown->list-filter#clear">
19
- <span class="filter-count mono muted" data-list-filter-target="count"></span>
16
+ <div class="drill operation-workspace" data-controller="list-filter">
17
+ <%== render("_chats_master", locals: { active_id: nil }) %>
18
+
19
+ <turbo-frame id="session-detail" class="drill-pane drill-detail">
20
+ <div class="drill-pane-head">
21
+ <div class="title">
22
+ <strong>Pick a conversation</strong>
23
+ <span class="count">select on the left, or start a new one in the playground</span>
24
+ </div>
20
25
  </div>
21
- <div class="card">
22
- <ul class="history">
23
- <% @sessions.each do |s| %>
24
- <% preview = session_preview(s) %>
25
- <li data-list-filter-target="item" data-filter-text="<%= s.id %> <%= preview %>">
26
- <a href="/studio/sessions/<%= Rack::Utils.escape(s.id) %>">
27
- <span class="mono"><%= s.id[0, 12] %></span>
28
- </a>
29
- <span class="muted"><%= Array(s.messages).size %> msg · <%= s.updated_at %></span>
30
- <% if preview %><span class="preview muted"><%= preview[0, 90] %></span><% end %>
31
- </li>
32
- <% end %>
33
- </ul>
26
+ <div class="drill-empty">
27
+ <div><h2>Read the conversation</h2><p>Review messages, the latest briefing and stage history. New messages appear as they arrive.</p></div>
34
28
  </div>
35
- <div class="empty" data-list-filter-target="empty" hidden>No conversations match your filter.</div>
36
- </div>
37
- <% end %>
29
+ </turbo-frame>
30
+ </div>
@@ -1,53 +1,94 @@
1
- <header class="page-head">
2
- <div>
3
- <nav class="crumbs"><a href="/studio/customers">customers</a></nav>
4
- <h1><span class="mono"><%= @cell[:customer] || @cell[:scope] %></span><span class="sub">customer memory</span></h1>
5
- </div>
6
- <div class="actions">
7
- <form method="post" action="<%= customer_path(@cell[:scope]) %>/export">
8
- <%== csrf_tag %>
9
- <button type="submit" class="btn" data-turbo-submits-with="Exporting…">Export</button>
10
- </form>
11
- <form method="post" action="<%= customer_path(@cell[:scope]) %>/forget"
12
- data-turbo-confirm="Forget <%= @cell[:customer] %>? Their memory cell AND their sessions are purged. This cannot be undone.">
13
- <%== csrf_tag %>
14
- <button type="submit" class="btn danger" data-turbo-submits-with="Forgetting…">Forget</button>
15
- </form>
16
- </div>
17
- </header>
1
+ <%# Customer detail — the DETAIL pane of the Customers miller shell.
2
+ Full page (direct visit/refresh): the two-column shell — master list plus
3
+ this pane. Turbo-Frame request (a row click, a form submission): the pane alone,
4
+ so the master's scroll state and selection survive every interaction. %>
5
+ <%# Roda renders with plain locals — no local_assigns hash (Rails-ism). %>
6
+ <% frame_only = defined?(frame_only) && frame_only %>
7
+ <% unless frame_only %>
8
+ <header class="page-head operation-head">
9
+ <div>
10
+ <nav class="crumbs"><a href="/studio/customers<%= @agent ? "?agent=#{Rack::Utils.escape(@agent)}" : "" %>">customers</a></nav>
11
+ <h1><span class="mono"><%= @cell[:customer] || @cell[:scope] %></span><span class="sub">customer memory</span></h1>
12
+ </div>
13
+ </header>
14
+
15
+ <div class="drill operation-workspace" data-controller="list-filter">
16
+ <%== render("_customers_master", locals: { active_scope: @cell[:scope] }) %>
17
+ <% end %>
18
+ <turbo-frame id="customer-detail" class="drill-pane drill-detail operation-detail">
19
+ <%# A frame response never sees the layout, so the flash strip lives here
20
+ too — without it, save confirmations would vanish on frame submits
21
+ (the layout's copy only renders on full pages). %>
22
+ <% if request.env["HTTP_TURBO_FRAME"] == "customer-detail" %>
23
+ <% if flash["error"] %><div class="flash err" role="alert"><%= flash["error"] %></div><% end %>
24
+ <% if flash["notice"] %><div class="flash ok" role="status"><%= flash["notice"] %></div><% end %>
25
+ <% end %>
26
+ <div class="drill-pane-head">
27
+ <div class="title">
28
+ <strong class="mono"><%= @cell[:customer] || @cell[:scope] %></strong>
29
+ <span class="count"><%= @cell[:tenant] %> · <%= @facts.size %> saved facts</span>
30
+ </div>
31
+ <div class="actions">
32
+ <form method="post" action="<%= customer_path(@cell[:scope]) %>/export" data-turbo="false">
33
+ <%== csrf_tag %>
34
+ <% if @agent %><input type="hidden" name="agent" value="<%= @agent %>"><% end %>
35
+ <button type="submit" class="btn btn-sm" data-turbo-submits-with="Exporting…">Export</button>
36
+ </form>
37
+ <form method="post" action="<%= customer_path(@cell[:scope]) %>/forget" data-turbo-frame="_top"
38
+ data-turbo-confirm="Forget <%= @cell[:customer] %>? Their memory cell AND their sessions are purged. This cannot be undone.">
39
+ <%== csrf_tag %>
40
+ <% if @agent %><input type="hidden" name="agent" value="<%= @agent %>"><% end %>
41
+ <button type="submit" class="btn btn-sm danger" data-turbo-submits-with="Forgetting…">Forget</button>
42
+ </form>
43
+ </div>
44
+ </div>
45
+ <div class="drill-pane-body">
18
46
 
19
47
  <section class="section">
20
48
  <div class="section-head">
21
49
  <h2>Facts</h2>
22
- <p class="muted">what the next turn reads — expired facts are never injected .</p>
50
+ <p class="muted">Saved values used in future conversations. Expired facts are excluded.</p>
23
51
  </div>
24
52
  <div class="card">
25
53
  <% if @facts.empty? %>
26
54
  <p class="muted">No facts.</p>
27
55
  <% else %>
28
- <ul class="kv">
56
+ <div class="operation-facts">
29
57
  <% @facts.each do |f| %>
30
- <li>
31
- <span class="k mono"><%= f.key %></span>
32
- <span class="v"><%= f.value %></span>
33
- <span class="muted mono"><%= f.origin %> · <%= f.created_at %><%= f.expires_at ? " · expires #{f.expires_at}" : "" %></span>
34
- <form method="post" action="<%= customer_path(@cell[:scope]) %>/forget-fact"
35
- data-turbo-confirm="Forget &quot;<%= f.key %>&quot;? The next turn stops seeing it.">
36
- <%== csrf_tag %>
37
- <input type="hidden" name="key" value="<%= f.key %>">
38
- <button type="submit" class="ghost danger" data-turbo-submits-with="Forgetting…">Forget</button>
39
- </form>
40
- </li>
58
+ <article class="operation-fact">
59
+ <div class="operation-summary">
60
+ <div><h3><%= f.key %></h3><p class="operation-fact-value"><%= f.value %></p></div>
61
+ <form method="post" action="<%= customer_path(@cell[:scope]) %>/forget-fact"
62
+ data-turbo-confirm="Forget &quot;<%= f.key %>&quot;? The next turn stops seeing it.">
63
+ <%== csrf_tag %>
64
+ <% if @agent %><input type="hidden" name="agent" value="<%= @agent %>"><% end %>
65
+ <input type="hidden" name="key" value="<%= f.key %>">
66
+ <button type="submit" class="ghost danger" data-turbo-submits-with="Forgetting…">Forget</button>
67
+ </form>
68
+ </div>
69
+ <details class="operation-fact-source">
70
+ <summary>Source and dates</summary>
71
+ <p class="muted"><%= f.origin %> · saved <%= f.created_at %><%= f.expires_at ? " · expires #{f.expires_at}" : " · no expiry" %></p>
72
+ </details>
73
+ </article>
41
74
  <% end %>
42
- </ul>
75
+ </div>
43
76
  <% end %>
44
- <form class="inline-form" method="post" action="<%= customer_path(@cell[:scope]) %>/fact">
45
- <%== csrf_tag %>
46
- <input type="text" name="key" placeholder="key (e.g. size)" required>
47
- <input type="text" name="value" placeholder="value" required>
48
- <input type="text" name="expires_at" placeholder="expires_at (ISO8601, optional)">
49
- <button type="submit" class="primary" data-turbo-submits-with="Saving…">Save fact</button>
50
- </form>
77
+ <details class="operation-fact-editor">
78
+ <summary>Add or update a fact</summary>
79
+ <p class="muted">Saving an existing key replaces its value for future conversations.</p>
80
+ <form method="post" action="<%= customer_path(@cell[:scope]) %>/fact">
81
+ <%== csrf_tag %>
82
+ <% if @agent %><input type="hidden" name="agent" value="<%= @agent %>"><% end %>
83
+ <div class="operation-fields">
84
+ <label for="customer-fact-key">Key<input id="customer-fact-key" type="text" name="key" placeholder="e.g. size" required></label>
85
+ <label for="customer-fact-value">Value<input id="customer-fact-value" type="text" name="value" required></label>
86
+ </div>
87
+ <label for="customer-fact-expiry">Expires at <span class="muted">(optional, ISO8601)</span></label>
88
+ <input id="customer-fact-expiry" type="text" name="expires_at" placeholder="2026-12-31T23:59:59Z">
89
+ <button type="submit" class="primary" data-turbo-submits-with="Saving…">Save fact</button>
90
+ </form>
91
+ </details>
51
92
  </div>
52
93
  </section>
53
94
 
@@ -68,10 +109,9 @@
68
109
  </div>
69
110
  </section>
70
111
 
71
- <section class="section">
72
- <div class="section-head">
73
- <h2>Audit <span class="muted">(operator mutations — digests, never content)</span></h2>
74
- </div>
112
+ <details class="card operation-diagnostics">
113
+ <summary>Audit history</summary>
114
+ <p class="muted">Operator changes are recorded as digests, without storing the content.</p>
75
115
  <div class="card">
76
116
  <% if @audit.empty? %>
77
117
  <p class="muted">No operator mutations recorded.</p>
@@ -91,4 +131,9 @@
91
131
  </ul>
92
132
  <% end %>
93
133
  </div>
94
- </section>
134
+ </details>
135
+ </div>
136
+ </turbo-frame>
137
+ <% unless frame_only %>
138
+ </div>
139
+ <% end %>
@@ -1,32 +1,22 @@
1
- <header class="page-head">
2
- <div>
3
- <nav class="crumbs"><span>customers</span></nav>
4
- <h1>Customers<span class="sub"><%= @rows.size %> with memory</span></h1>
5
- </div>
1
+ <header class="page-head operation-head">
2
+ <h1>Customers<span class="sub"><%= @rows.size %> with memory</span></h1>
6
3
  <div class="head-actions">
7
4
  <%== agent_filter_form("/studio/customers", @agent) %>
8
5
  </div>
9
6
  </header>
10
7
 
11
- <% if @rows.empty? %>
12
- <div class="empty">No customers with memory yet — facts appear here when a turn or the operator writes to a customer cell.</div>
13
- <% else %>
14
- <% @by_tenant.each do |tenant, rows| %>
15
- <section class="section">
16
- <div class="section-head">
17
- <h2><%= tenant.nil? ? "Shared (no tenant)" : tenant %></h2>
18
- <p class="muted"><%= rows.size %> customer cell(s)</p>
19
- </div>
20
- <div class="card">
21
- <ul class="history">
22
- <% rows.each do |row| %>
23
- <li>
24
- <a href="<%= customer_path(row[:cell][:scope]) %>"><span class="mono"><%= row[:cell][:customer] %></span></a>
25
- <span class="preview muted"><%= row[:count] %> fact(s)</span>
26
- </li>
27
- <% end %>
28
- </ul>
8
+ <div class="drill operation-workspace" data-controller="list-filter">
9
+ <%== render("_customers_master", locals: { active_scope: nil }) %>
10
+
11
+ <turbo-frame id="customer-detail" class="drill-pane drill-detail">
12
+ <div class="drill-pane-head">
13
+ <div class="title">
14
+ <strong>Pick a customer</strong>
15
+ <span class="count">review memory used in future conversations</span>
29
16
  </div>
30
- </section>
31
- <% end %>
32
- <% end %>
17
+ </div>
18
+ <div class="drill-empty">
19
+ <div><h2>Customer memory</h2><p>Select a customer to read saved facts and notes, add a fact or review changes.</p></div>
20
+ </div>
21
+ </turbo-frame>
22
+ </div>
@@ -1,11 +1,13 @@
1
+ <div class="reports-page reports-evals">
1
2
  <header class="page-head">
2
3
  <div>
3
- <nav class="crumbs"><span>evals</span></nav>
4
- <h1>Evals<span class="sub"><%= @cases.size %> case(s)</span></h1>
4
+ <%== crumbs_for("Evals") %>
5
+ <h1>Evals<span class="sub">Cases that check your agents</span></h1>
5
6
  </div>
6
7
  <div class="head-actions">
7
8
  <%== agent_filter_form("/studio/evals", @filter) %>
8
- <a class="btn" href="/studio/settings?s=evals">Judges →</a>
9
+ <a class="btn" href="/studio/evals<%= "?agent=#{Rack::Utils.escape(@filter)}" if @filter %>">New case</a>
10
+ <a class="btn" href="/studio/settings?s=evals">Configure judges</a>
9
11
  </div>
10
12
  </header>
11
13
 
@@ -17,9 +19,12 @@
17
19
  </div>
18
20
  <% end %>
19
21
 
20
- <div class="drill">
22
+ <div class="drill report-workspace" data-controller="list-filter">
21
23
  <aside class="drill-pane drill-master">
22
- <div class="drill-pane-head"><div class="title"><strong>Cases</strong></div></div>
24
+ <div class="drill-pane-head"><div class="title"><strong>Cases <span class="count"><%= @cases.size %> case(s)</span></strong></div></div>
25
+ <div class="drill-search">
26
+ <input type="search" class="filter-input" placeholder="Search cases…" aria-label="Filter cases by ID, agent or opening message" data-list-filter-target="query" data-action="input->list-filter#filter keydown->list-filter#clear">
27
+ </div>
23
28
  <nav class="drill-pane-body drill-list" aria-label="eval cases">
24
29
  <% if @cases.empty? %>
25
30
  <div class="empty">No case yet. Seed the corpus with <code>insika evals:import</code>, or write one on the right.</div>
@@ -27,12 +32,13 @@
27
32
  <% @by_agent.each do |agent, cases| %>
28
33
  <div class="nav-label"><%= agent %></div>
29
34
  <% cases.each do |g| %>
30
- <a class="drill-item<%== ' active' if @case && @case.id == g.id %>" href="/studio/evals?id=<%= Rack::Utils.escape(g.id) %>">
35
+ <a class="drill-item<%== ' active' if @case && @case.id == g.id %>" href="/studio/evals?<%= Rack::Utils.build_query({ "id" => g.id, "agent" => @filter }.compact) %>" data-list-filter-target="item" data-filter-text="<%= g.id %> <%= agent %> <%= g.opens_with %>"<%== ' aria-current="page"' if @case && @case.id == g.id %>>
31
36
  <div class="drill-item-name"><span class="txt mono"><%= g.id %></span><% if g.rubric.to_s.strip.empty? %><span class="pill info plain">no rubric</span><% end %><% if g.simulated? %><span class="pill accent plain">simulated</span><% end %></div>
32
37
  <div class="drill-item-sub"><%= g.opens_with.to_s %></div>
33
38
  </a>
34
39
  <% end %>
35
40
  <% end %>
41
+ <% if @cases.any? %><div class="empty" data-list-filter-target="empty" hidden>No cases match your search.</div><% end %>
36
42
  </nav>
37
43
  </aside>
38
44
 
@@ -51,36 +57,48 @@
51
57
  </div>
52
58
  </div>
53
59
  <div class="drill-pane-body">
54
- <p class="muted">
55
- A case is data, in the same YAML the corpus files hold — so this is what a pull
56
- request reviews. <code>expect.rubric</code> is plain language and is the part a
57
- judge scores; <code>tools_called</code> and <code>must_not</code> are checked
58
- deterministically, with no model involved. Saved through
59
- <code>write_golden</code> and validated on the way in.
60
- </p>
61
- <form id="set-golden" method="post" action="/studio/evals">
60
+ <p class="muted">Define the conversation and the behavior you expect. Saving validates the case before adding it to your corpus.</p>
61
+ <details class="report-details">
62
+ <summary>Case format and scoring</summary>
63
+ <p>Cases use the same YAML as the corpus files. <code>expect.rubric</code> describes what the judge scores; <code>tools_called</code> and <code>must_not</code> are checked without a model.</p>
64
+ <p>Import cases with <code>insika evals:import</code>. Configure judges in Settings.</p>
65
+ </details>
66
+ <%
67
+ # Erubi's tag scanner is a plain text scanner: it has no notion of Ruby
68
+ # comments, strings or heredocs, so it closes a code tag on the first
69
+ # literal percent-angle-bracket close marker it sees, wherever it is.
70
+ # A heredoc opened here therefore has to open, hold its whole body and
71
+ # close ALL inside this one tag, with that close marker appearing
72
+ # nowhere in between (not even in a comment) - otherwise the template
73
+ # text and its compiled buffer-append boilerplate between two tags
74
+ # gets spliced into the middle of the heredoc's own string value,
75
+ # which is how compiler internals ended up leaking into this textarea.
76
+ default_case_yaml = <<~YAML
77
+ id: my-agent-coupon
78
+ agent: my-agent
79
+ turns:
80
+ - user: "tem cupom de desconto?"
81
+ expect:
82
+ tools_called:
83
+ - search_voucher
84
+ must_not:
85
+ - pii_leak
86
+ - tool_error
87
+ rubric: |
88
+ Consults the active coupon with the tool and says what it is — does NOT
89
+ invent a code or a discount. If there is none, says so kindly.
90
+ min_score: 0.7
91
+ YAML
92
+ %>
93
+ <form id="set-golden" method="post" action="/studio/evals" data-controller="dirty-guard">
62
94
  <%== csrf_tag %>
63
95
  <input type="hidden" name="id" value="<%= @case&.id %>">
64
96
  <label>case (YAML)
65
- <textarea name="yaml" rows="22" spellcheck="false" class="mono"><%= @case_yaml || <<~YAML %>
66
- id: my-agent-coupon
67
- agent: my-agent
68
- turns:
69
- - user: "tem cupom de desconto?"
70
- expect:
71
- tools_called:
72
- - search_voucher
73
- must_not:
74
- - pii_leak
75
- - tool_error
76
- rubric: |
77
- Consults the active coupon with the tool and says what it is — does NOT
78
- invent a code or a discount. If there is none, says so kindly.
79
- min_score: 0.7
80
- YAML
81
- </textarea>
97
+ <textarea name="yaml" rows="22" spellcheck="false" class="mono"><%= @case_yaml || default_case_yaml %></textarea>
82
98
  </label>
83
99
  </form>
84
100
  </div>
85
101
  </section>
86
102
  </div>
103
+
104
+ </div>