insika 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +295 -0
  3. data/LICENSE +21 -0
  4. data/README.md +136 -2
  5. data/bin/insika +351 -0
  6. data/docs/AGENTS.md +494 -0
  7. data/docs/ARCHITECTURE.md +333 -0
  8. data/docs/BENCHMARK.md +114 -0
  9. data/docs/CHANNELS.md +453 -0
  10. data/docs/CONTEXT.md +100 -0
  11. data/docs/DEPLOY.md +334 -0
  12. data/docs/EMBEDDING.md +194 -0
  13. data/docs/EVALS.md +273 -0
  14. data/docs/LOADTEST.md +231 -0
  15. data/docs/OBSERVABILITY.md +365 -0
  16. data/docs/PLUGINS.md +211 -0
  17. data/docs/REFINEMENT.md +477 -0
  18. data/docs/RELEASING.md +70 -0
  19. data/docs/RUNNING-LOCAL.md +153 -0
  20. data/docs/SANDBOX.md +114 -0
  21. data/docs/SECURITY.md +362 -0
  22. data/docs/SKILLS.md +98 -0
  23. data/docs/TOOLS.md +302 -0
  24. data/docs/WHY.md +137 -0
  25. data/docs/WORKFLOWS.md +225 -0
  26. data/docs/build.md +14 -0
  27. data/docs/index.md +68 -0
  28. data/docs/onboarding/start.md +126 -0
  29. data/docs/operate.md +12 -0
  30. data/docs/ship.md +10 -0
  31. data/docs/understand.md +10 -0
  32. data/lib/insika/agent_file_store.rb +125 -0
  33. data/lib/insika/agent_profile.rb +188 -0
  34. data/lib/insika/allowlist.rb +28 -0
  35. data/lib/insika/baseline_store.rb +74 -0
  36. data/lib/insika/capability/resolved_tool.rb +34 -0
  37. data/lib/insika/capability_registry.rb +112 -0
  38. data/lib/insika/channel_delivery.rb +150 -0
  39. data/lib/insika/channel_registry.rb +30 -0
  40. data/lib/insika/channels/relay.rb +178 -0
  41. data/lib/insika/channels/web/widget.js +283 -0
  42. data/lib/insika/channels/web.rb +211 -0
  43. data/lib/insika/chat_builder.rb +254 -0
  44. data/lib/insika/checkpoint.rb +13 -0
  45. data/lib/insika/checkpoint_store.rb +153 -0
  46. data/lib/insika/coercion.rb +50 -0
  47. data/lib/insika/command.rb +32 -0
  48. data/lib/insika/command_bus.rb +39 -0
  49. data/lib/insika/commands/agent_payload.rb +41 -0
  50. data/lib/insika/commands/approve_action.rb +46 -0
  51. data/lib/insika/commands/cancel_task.rb +33 -0
  52. data/lib/insika/commands/create_agent.rb +54 -0
  53. data/lib/insika/commands/create_session.rb +67 -0
  54. data/lib/insika/commands/delete_agent.rb +33 -0
  55. data/lib/insika/commands/delete_agent_file.rb +50 -0
  56. data/lib/insika/commands/delete_data_tool.rb +33 -0
  57. data/lib/insika/commands/delete_llm_provider.rb +36 -0
  58. data/lib/insika/commands/delete_mcp.rb +30 -0
  59. data/lib/insika/commands/delete_system_file.rb +29 -0
  60. data/lib/insika/commands/gate_refinement.rb +245 -0
  61. data/lib/insika/commands/import_mcp_tools.rb +48 -0
  62. data/lib/insika/commands/import_tools.rb +81 -0
  63. data/lib/insika/commands/memory_add_note.rb +32 -0
  64. data/lib/insika/commands/memory_forget_fact.rb +32 -0
  65. data/lib/insika/commands/memory_put_fact.rb +35 -0
  66. data/lib/insika/commands/pause_task.rb +29 -0
  67. data/lib/insika/commands/resolve_refinement.rb +126 -0
  68. data/lib/insika/commands/restore_agent_file.rb +36 -0
  69. data/lib/insika/commands/restore_data_tool.rb +34 -0
  70. data/lib/insika/commands/restore_system_file.rb +31 -0
  71. data/lib/insika/commands/resume_task.rb +85 -0
  72. data/lib/insika/commands/run_refinement.rb +133 -0
  73. data/lib/insika/commands/send_message.rb +150 -0
  74. data/lib/insika/commands/set_agent_tools.rb +39 -0
  75. data/lib/insika/commands/set_skill_agents.rb +71 -0
  76. data/lib/insika/commands/trigger_workflow.rb +80 -0
  77. data/lib/insika/commands/update_agent.rb +49 -0
  78. data/lib/insika/commands/update_settings.rb +33 -0
  79. data/lib/insika/commands/upsert_llm_provider.rb +34 -0
  80. data/lib/insika/commands/upsert_mcp.rb +32 -0
  81. data/lib/insika/commands/write_agent_file.rb +57 -0
  82. data/lib/insika/commands/write_data_tool.rb +43 -0
  83. data/lib/insika/commands/write_golden.rb +58 -0
  84. data/lib/insika/commands/write_skill.rb +50 -0
  85. data/lib/insika/commands/write_system_file.rb +31 -0
  86. data/lib/insika/config_store.rb +85 -0
  87. data/lib/insika/context/builder.rb +166 -0
  88. data/lib/insika/context/catalog_provider.rb +23 -0
  89. data/lib/insika/context/fragment.rb +19 -0
  90. data/lib/insika/context/priority.rb +29 -0
  91. data/lib/insika/context/provider.rb +19 -0
  92. data/lib/insika/context/providers/memory.rb +60 -0
  93. data/lib/insika/context/providers/prompt.rb +105 -0
  94. data/lib/insika/context/providers/request.rb +32 -0
  95. data/lib/insika/context/providers/session.rb +108 -0
  96. data/lib/insika/context/providers/skill.rb +20 -0
  97. data/lib/insika/context/providers/tool_search.rb +20 -0
  98. data/lib/insika/delegation_store.rb +153 -0
  99. data/lib/insika/doctor.rb +294 -0
  100. data/lib/insika/dsl/definition.rb +55 -0
  101. data/lib/insika/dsl/runtime.rb +379 -0
  102. data/lib/insika/dsl/server_boot.rb +97 -0
  103. data/lib/insika/dsl/system.rb +93 -0
  104. data/lib/insika/dsl/workflow_adapter.rb +59 -0
  105. data/lib/insika/dsl.rb +307 -0
  106. data/lib/insika/edge_limiter.rb +130 -0
  107. data/lib/insika/egress_guard.rb +75 -0
  108. data/lib/insika/env_schema.rb +246 -0
  109. data/lib/insika/errors.rb +145 -0
  110. data/lib/insika/evals/assertions.rb +247 -0
  111. data/lib/insika/evals/baseline.rb +69 -0
  112. data/lib/insika/evals/golden.rb +172 -0
  113. data/lib/insika/evals/judge.rb +225 -0
  114. data/lib/insika/evals/pairwise.rb +178 -0
  115. data/lib/insika/evals/report.rb +115 -0
  116. data/lib/insika/evals/runner.rb +141 -0
  117. data/lib/insika/evals/transport.rb +178 -0
  118. data/lib/insika/event.rb +18 -0
  119. data/lib/insika/event_stream.rb +114 -0
  120. data/lib/insika/executor.rb +1680 -0
  121. data/lib/insika/frontmatter.rb +42 -0
  122. data/lib/insika/golden_store.rb +145 -0
  123. data/lib/insika/hooks.rb +48 -0
  124. data/lib/insika/http_client.rb +63 -0
  125. data/lib/insika/inbound_log.rb +84 -0
  126. data/lib/insika/llm_configurator.rb +99 -0
  127. data/lib/insika/llm_provider_store.rb +83 -0
  128. data/lib/insika/mcp_http_client.rb +67 -0
  129. data/lib/insika/mcp_store.rb +115 -0
  130. data/lib/insika/mcp_tool_ingestor.rb +143 -0
  131. data/lib/insika/memory_store.rb +93 -0
  132. data/lib/insika/message_origin.rb +76 -0
  133. data/lib/insika/middleware.rb +36 -0
  134. data/lib/insika/model_policy.rb +52 -0
  135. data/lib/insika/model_resolver.rb +176 -0
  136. data/lib/insika/model_selection.rb +114 -0
  137. data/lib/insika/onboarding.rb +208 -0
  138. data/lib/insika/outbox_store.rb +166 -0
  139. data/lib/insika/overlay_tool_registry.rb +103 -0
  140. data/lib/insika/pack.rb +102 -0
  141. data/lib/insika/pack_importer.rb +121 -0
  142. data/lib/insika/pending_action_store.rb +120 -0
  143. data/lib/insika/plugin/loader.rb +356 -0
  144. data/lib/insika/plugin.rb +35 -0
  145. data/lib/insika/policy/engine.rb +83 -0
  146. data/lib/insika/policy/policy.rb +120 -0
  147. data/lib/insika/policy_registry.rb +23 -0
  148. data/lib/insika/profile_source.rb +137 -0
  149. data/lib/insika/prompt_catalog.rb +61 -0
  150. data/lib/insika/queue_policy.rb +167 -0
  151. data/lib/insika/recovery.rb +127 -0
  152. data/lib/insika/refinement/candidate.rb +159 -0
  153. data/lib/insika/refinement/evidence_collector.rb +371 -0
  154. data/lib/insika/refinement/gate.rb +234 -0
  155. data/lib/insika/refinement/panel.rb +222 -0
  156. data/lib/insika/refinement/proposer.rb +262 -0
  157. data/lib/insika/refinement_store.rb +295 -0
  158. data/lib/insika/registry.rb +59 -0
  159. data/lib/insika/safety/config.rb +109 -0
  160. data/lib/insika/safety/detectors.rb +176 -0
  161. data/lib/insika/safety/factory.rb +102 -0
  162. data/lib/insika/safety/input_guardrail.rb +87 -0
  163. data/lib/insika/safety/moderator.rb +86 -0
  164. data/lib/insika/safety/output_filter.rb +79 -0
  165. data/lib/insika/safety/output_validator.rb +101 -0
  166. data/lib/insika/safety/safe_responses.rb +47 -0
  167. data/lib/insika/sandbox/boundary.rb +93 -0
  168. data/lib/insika/sandbox/docker.rb +74 -0
  169. data/lib/insika/sandbox/local.rb +33 -0
  170. data/lib/insika/sandbox/runner.rb +80 -0
  171. data/lib/insika/sandbox.rb +85 -0
  172. data/lib/insika/schema_guard.rb +147 -0
  173. data/lib/insika/secret_masking.rb +34 -0
  174. data/lib/insika/server/a2a/agent_card.rb +27 -0
  175. data/lib/insika/server/a2a/app.rb +112 -0
  176. data/lib/insika/server/a2a/client.rb +101 -0
  177. data/lib/insika/server/a2a/errors.rb +32 -0
  178. data/lib/insika/server/a2a/http.rb +42 -0
  179. data/lib/insika/server/a2a/message.rb +27 -0
  180. data/lib/insika/server/a2a/protocol.rb +45 -0
  181. data/lib/insika/server/a2a/remotes.rb +25 -0
  182. data/lib/insika/server/a2a/task_projection.rb +40 -0
  183. data/lib/insika/server/admin_auth.rb +29 -0
  184. data/lib/insika/server/app.rb +850 -0
  185. data/lib/insika/server/boot.rb +119 -0
  186. data/lib/insika/server/rack_app.rb +110 -0
  187. data/lib/insika/server/responses.rb +155 -0
  188. data/lib/insika/server/sse_body.rb +96 -0
  189. data/lib/insika/session_actor.rb +162 -0
  190. data/lib/insika/session_store.rb +143 -0
  191. data/lib/insika/settings_store.rb +154 -0
  192. data/lib/insika/shutdown.rb +125 -0
  193. data/lib/insika/skill_catalog.rb +113 -0
  194. data/lib/insika/skill_store.rb +79 -0
  195. data/lib/insika/steer_injector.rb +110 -0
  196. data/lib/insika/store.rb +52 -0
  197. data/lib/insika/stores/memory.rb +123 -0
  198. data/lib/insika/stores/sqlite.rb +183 -0
  199. data/lib/insika/studio/app.rb +1571 -0
  200. data/lib/insika/studio/assets/dist/application.css +1 -0
  201. data/lib/insika/studio/assets/dist/application.js +69 -0
  202. data/lib/insika/studio/forms.rb +340 -0
  203. data/lib/insika/studio/nav_icons.rb +31 -0
  204. data/lib/insika/studio/views/_message.erb +44 -0
  205. data/lib/insika/studio/views/agent_detail.erb +285 -0
  206. data/lib/insika/studio/views/agents.erb +63 -0
  207. data/lib/insika/studio/views/approvals.erb +41 -0
  208. data/lib/insika/studio/views/chats.erb +34 -0
  209. data/lib/insika/studio/views/evals.erb +83 -0
  210. data/lib/insika/studio/views/home.erb +72 -0
  211. data/lib/insika/studio/views/layout.erb +94 -0
  212. data/lib/insika/studio/views/login.erb +17 -0
  213. data/lib/insika/studio/views/mcp.erb +91 -0
  214. data/lib/insika/studio/views/not_found.erb +5 -0
  215. data/lib/insika/studio/views/playground.erb +47 -0
  216. data/lib/insika/studio/views/refinement.erb +234 -0
  217. data/lib/insika/studio/views/session.erb +62 -0
  218. data/lib/insika/studio/views/settings.erb +173 -0
  219. data/lib/insika/studio/views/skills.erb +86 -0
  220. data/lib/insika/studio/views/system_files.erb +65 -0
  221. data/lib/insika/studio/views/task.erb +105 -0
  222. data/lib/insika/studio/views/tasks.erb +33 -0
  223. data/lib/insika/studio/views/tool_edit.erb +107 -0
  224. data/lib/insika/studio/views/tools.erb +89 -0
  225. data/lib/insika/subagent_graph.rb +96 -0
  226. data/lib/insika/system_file_store.rb +96 -0
  227. data/lib/insika/task_actor.rb +128 -0
  228. data/lib/insika/task_store.rb +250 -0
  229. data/lib/insika/telemetry/pricing.rb +104 -0
  230. data/lib/insika/telemetry/recorder.rb +228 -0
  231. data/lib/insika/telemetry.rb +127 -0
  232. data/lib/insika/testing/store_contract.rb +270 -0
  233. data/lib/insika/token_estimator.rb +16 -0
  234. data/lib/insika/tool_assembly.rb +140 -0
  235. data/lib/insika/tool_catalog.rb +89 -0
  236. data/lib/insika/tool_definition.rb +518 -0
  237. data/lib/insika/tool_envelope.rb +140 -0
  238. data/lib/insika/tool_manifest.rb +218 -0
  239. data/lib/insika/tool_registry.rb +21 -0
  240. data/lib/insika/tool_store.rb +135 -0
  241. data/lib/insika/tool_trace_store.rb +92 -0
  242. data/lib/insika/tools/a2a_remote.rb +48 -0
  243. data/lib/insika/tools/agent_enum.rb +68 -0
  244. data/lib/insika/tools/concurrency.rb +54 -0
  245. data/lib/insika/tools/data_defined_tool.rb +220 -0
  246. data/lib/insika/tools/load_skill.rb +41 -0
  247. data/lib/insika/tools/remember.rb +53 -0
  248. data/lib/insika/tools/subagent.rb +75 -0
  249. data/lib/insika/tools/subagents.rb +77 -0
  250. data/lib/insika/tools/tool_search.rb +94 -0
  251. data/lib/insika/turn_output.rb +139 -0
  252. data/lib/insika/turn_state.rb +158 -0
  253. data/lib/insika/turn_timing.rb +56 -0
  254. data/lib/insika/usage_ledger.rb +47 -0
  255. data/lib/insika/version.rb +3 -1
  256. data/lib/insika/wiring/graph.rb +198 -0
  257. data/lib/insika/workflow.rb +185 -0
  258. data/lib/insika/workflow_registry.rb +33 -0
  259. data/lib/insika.rb +203 -4
  260. metadata +395 -8
@@ -0,0 +1,94 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="<%= theme_pref %>">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="csp-nonce" content="<%= csp_nonce %>">
7
+ <title>Insika Studio</title>
8
+ <link rel="stylesheet" href="<%= asset_path("application.css") %>">
9
+ <script type="module" src="<%= asset_path("application.js") %>" defer></script>
10
+ </head>
11
+ <body>
12
+ <% if authenticated? %>
13
+ <div class="app-shell">
14
+ <aside class="sidebar">
15
+ <a class="brand" href="/studio/home"><span class="dot"></span>Insika <small>Studio</small></a>
16
+
17
+ <% nav_sections.each do |group, items| %>
18
+ <div class="nav-group">
19
+ <% unless group.empty? %><div class="nav-label"><%= group %></div><% end %>
20
+ <nav class="nav" aria-label="<%= group %>">
21
+ <% items.each do |label, href, icon| %>
22
+ <a href="<%= href %>"<%== ' aria-current="page"' if nav_active?(href) %>>
23
+ <%== nav_icon(icon) %><span class="lbl"><%= label %></span>
24
+ </a>
25
+ <% end %>
26
+ </nav>
27
+ </div>
28
+ <% end %>
29
+
30
+ <div class="sidebar-foot">
31
+ <div class="health-card">
32
+ <div class="row"><span class="pill ok online plain">runtime online</span></div>
33
+ <div class="stat">
34
+ <% health_parts.each do |label, value| %>
35
+ <span class="stat-row"><b><%= value %></b> <%= label %></span>
36
+ <% end %>
37
+ </div>
38
+ </div>
39
+ <div class="foot-actions">
40
+ <div class="theme-switch" data-controller="theme" role="group" aria-label="theme">
41
+ <% { "auto" => "auto", "light" => "light", "dark" => "dark" }.each do |val, label| %>
42
+ <button type="button" class="theme-opt" data-theme-target="opt"
43
+ data-theme-value-param="<%= val %>" data-action="theme#set"><%= label %></button>
44
+ <% end %>
45
+ </div>
46
+ <form class="logout" method="post" action="/studio/logout">
47
+ <%== csrf_tag %>
48
+ <button type="submit" class="ghost">Sign out</button>
49
+ </form>
50
+ </div>
51
+ <div class="env-chip" title="runtime environment">
52
+ <span class="env-mark" aria-hidden="true"><%= env_label[0, 1].upcase %></span>
53
+ <div class="env-text">
54
+ <b><%= env_label %></b>
55
+ <small>environment</small>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </aside>
60
+
61
+ <div class="app-main">
62
+ <% if restart_needed? %>
63
+ <div class="banner" role="status">
64
+ <span><strong>Restart recommended.</strong> MCP changes take effect after the runtime restarts.</span>
65
+ <form method="post" action="/studio/restart-ack">
66
+ <%== csrf_tag %>
67
+ <input type="hidden" name="back" value="<%= request.script_name %><%= request.path %>">
68
+ <button type="submit" class="ghost">Dismiss</button>
69
+ </form>
70
+ </div>
71
+ <% end %>
72
+
73
+ <% if flash["error"] %>
74
+ <div class="flash err" role="alert"><%= flash["error"] %></div>
75
+ <% end %>
76
+ <% if flash["notice"] %>
77
+ <div class="flash ok" role="status"><%= flash["notice"] %></div>
78
+ <% end %>
79
+
80
+ <main class="page">
81
+ <%== yield %>
82
+ </main>
83
+ </div>
84
+ </div>
85
+ <% else %>
86
+ <% if flash["error"] %>
87
+ <div class="flash err" role="alert"><%= flash["error"] %></div>
88
+ <% end %>
89
+ <main class="page auth-page">
90
+ <%== yield %>
91
+ </main>
92
+ <% end %>
93
+ </body>
94
+ </html>
@@ -0,0 +1,17 @@
1
+ <div class="card auth-card">
2
+ <div class="brand big"><span class="dot"></span>Insika <small>Studio</small></div>
3
+ <p class="muted lead">Sign in with your operator token to manage your agents.</p>
4
+
5
+ <% if @error %>
6
+ <div class="flash err" role="alert"><%= @error %></div>
7
+ <% end %>
8
+
9
+ <form method="post" action="/studio/login">
10
+ <%== csrf_tag %>
11
+ <label>Operator token
12
+ <input type="password" name="token" autocomplete="current-password" autofocus
13
+ placeholder="ADMIN_TOKEN">
14
+ </label>
15
+ <div class="actions"><button type="submit" class="primary" data-turbo-submits-with="Signing in…">Sign in</button></div>
16
+ </form>
17
+ </div>
@@ -0,0 +1,91 @@
1
+ <header class="page-head">
2
+ <div>
3
+ <nav class="crumbs"><a href="/studio/agents">agents</a> / <span>mcp</span></nav>
4
+ <h1>MCP<span class="sub"><%= @instances.size %> instance(s) · credentials masked</span></h1>
5
+ </div>
6
+ </header>
7
+
8
+ <p class="muted matrix-hint">MCP server instances (durable config). Credentials in <code>env</code> never come back in plaintext — each value shows as <code><%= secret_sentinel %></code>; keeping it preserves the secret, changing it replaces, clearing the line removes it.</p>
9
+
10
+ <% if @instances.empty? %>
11
+ <div class="empty">No MCP instances. Add one below (e.g. <code>tavily</code>) to expose an MCP server to your agents' tools.</div>
12
+ <% end %>
13
+
14
+ <% @instances.each do |m| %>
15
+ <div class="card promptfile">
16
+ <div class="promptfile-head">
17
+ <strong class="mono"><%= m["name"] %>
18
+ <% if m["enabled"] %><span class="pill ok">on</span><% else %><span class="pill warn">off</span><% end %>
19
+ </strong>
20
+ <form method="post" action="/studio/mcp/delete" data-turbo-confirm="Remove MCP instance &quot;<%= m["name"] %>&quot;? Agents stop seeing its tools next turn.">
21
+ <%== csrf_tag %>
22
+ <input type="hidden" name="name" value="<%= m["name"] %>">
23
+ <button type="submit" class="ghost danger" data-turbo-submits-with="Removing…">Remove</button>
24
+ </form>
25
+ </div>
26
+ <form method="post" action="/studio/mcp">
27
+ <%== csrf_tag %>
28
+ <input type="hidden" name="name" value="<%= m["name"] %>">
29
+ <div class="grid-2">
30
+ <label>transport
31
+ <select name="transport">
32
+ <% %w[stdio http sse].each do |t| %>
33
+ <option value="<%= t %>"<%== " selected" if m["transport"] == t %>><%= t %></option>
34
+ <% end %>
35
+ </select>
36
+ </label>
37
+ <label class="check">
38
+ <input type="checkbox" name="enabled" value="1"<%== " checked" if m["enabled"] %>>
39
+ enabled
40
+ </label>
41
+ </div>
42
+ <label>command <span class="muted">(stdio)</span>
43
+ <input type="text" name="command" value="<%= m["command"] %>" placeholder="npx -y @modelcontextprotocol/server-...">
44
+ </label>
45
+ <label>url <span class="muted">(http/sse)</span>
46
+ <input type="text" name="url" value="<%= m["url"] %>" placeholder="https://mcp.example.com">
47
+ </label>
48
+ <label>description
49
+ <input type="text" name="description" value="<%= m["description"] %>">
50
+ </label>
51
+ <label>env <span class="muted">(one <code>KEY=value</code> per line)</span>
52
+ <textarea name="env" rows="4"><%= env_lines(m["env"]) %></textarea>
53
+ </label>
54
+ <div class="actions"><button type="submit" class="primary" data-turbo-submits-with="Saving…">Save instance</button></div>
55
+ </form>
56
+ </div>
57
+ <% end %>
58
+
59
+ <details class="card newfile">
60
+ <summary>+ new MCP instance</summary>
61
+ <form method="post" action="/studio/mcp">
62
+ <%== csrf_tag %>
63
+ <div class="grid-2">
64
+ <label>name <span class="muted">(slug)</span>
65
+ <input type="text" name="name" placeholder="tavily" required>
66
+ </label>
67
+ <label>transport
68
+ <select name="transport">
69
+ <% %w[stdio http sse].each do |t| %><option value="<%= t %>"><%= t %></option><% end %>
70
+ </select>
71
+ </label>
72
+ </div>
73
+ <label class="check">
74
+ <input type="checkbox" name="enabled" value="1" checked>
75
+ enabled
76
+ </label>
77
+ <label>command <span class="muted">(stdio)</span>
78
+ <input type="text" name="command" placeholder="npx -y @modelcontextprotocol/server-...">
79
+ </label>
80
+ <label>url <span class="muted">(http/sse)</span>
81
+ <input type="text" name="url" placeholder="https://mcp.example.com">
82
+ </label>
83
+ <label>description
84
+ <input type="text" name="description">
85
+ </label>
86
+ <label>env <span class="muted">(one <code>KEY=value</code> per line)</span>
87
+ <textarea name="env" rows="4" placeholder="TAVILY_API_KEY=tvly-…"></textarea>
88
+ </label>
89
+ <div class="actions"><button type="submit" class="primary" data-turbo-submits-with="Creating…">Create instance</button></div>
90
+ </form>
91
+ </details>
@@ -0,0 +1,5 @@
1
+ <section class="empty big">
2
+ <h1>404</h1>
3
+ <p class="muted">Page not found.</p>
4
+ <p><a class="btn ghost" href="/studio/agents">← back to agents</a></p>
5
+ </section>
@@ -0,0 +1,47 @@
1
+ <%# Playground — chat surface (T4): config bar on top, transcript scrolls, composer
2
+ pinned at the bottom. Same form fields/route + live-transcript targets as before;
3
+ fires the same send_message as the API. %>
4
+ <div class="chat"
5
+ data-controller="live-transcript"
6
+ data-live-transcript-session-value="<%= @session_id %>">
7
+ <form class="chat-form" method="post" action="/studio/playground">
8
+ <%== csrf_tag %>
9
+ <div class="chat-head">
10
+ <label class="chat-field">agent
11
+ <select name="agent">
12
+ <% @agents.each do |id| %>
13
+ <option value="<%= id %>"<%== ' selected' if id == @agent %>><%= id %></option>
14
+ <% end %>
15
+ </select>
16
+ </label>
17
+ <label class="chat-field">session
18
+ <input type="text" name="session_id" value="<%= @session_id %>" placeholder="empty = new conversation">
19
+ </label>
20
+ <span class="pill info chat-status" data-live-transcript-target="status">disconnected</span>
21
+ <details class="pin chat-pin">
22
+ <summary>overrides <span class="muted">— pin a NEW conversation (Chat › Agent › Model › platform)</span></summary>
23
+ <div class="row">
24
+ <label>model<input type="text" name="model" placeholder="leave empty = agent/platform default"></label>
25
+ <label>provider<input type="text" name="provider" placeholder="optional"></label>
26
+ <label>reasoning<%== thinking_select("thinking", nil, "agent/platform default") %></label>
27
+ </div>
28
+ </details>
29
+ </div>
30
+
31
+ <div class="chat-thread">
32
+ <div class="thread" data-live-transcript-target="stream">
33
+ <% Array(@history).each_with_index do |m, i| %><%== render("_message", locals: { m: m, prev: i.positive? ? @history[i - 1] : nil }) %><% end %>
34
+ <% if @sent_message %><%== render("_message", locals: { m: { "role" => "user", "content" => @sent_message, "at" => nil }, prev: nil }) %><% end %>
35
+ </div>
36
+ <div class="empty chat-empty" data-live-transcript-target="empty"<%== ' hidden' unless Array(@history).empty? && @sent_message.nil? %>>
37
+ <% if @session_id %>Waiting for turn events…<% else %>Send a message to begin a conversation.<% end %>
38
+ </div>
39
+ </div>
40
+
41
+ <div class="chat-composer">
42
+ <textarea name="message" rows="1" placeholder="Ask the agent something…" required
43
+ aria-label="message"></textarea>
44
+ <button type="submit" class="primary" data-turbo-submits-with="Sending…">Send</button>
45
+ </div>
46
+ </form>
47
+ </div>
@@ -0,0 +1,234 @@
1
+ <header class="page-head">
2
+ <div>
3
+ <nav class="crumbs"><span>refinement</span></nav>
4
+ <h1>Refinement<span class="sub"><%= @agent ? "#{@agent}" : "no agent" %></span></h1>
5
+ </div>
6
+ <a class="btn" href="/studio/chats">Chats →</a>
7
+ </header>
8
+
9
+ <% if @agents.empty? %>
10
+ <div class="empty">No agents yet. Create one first — a refinement run reads an agent's own traffic.</div>
11
+ <% else %>
12
+ <div class="card">
13
+ <form method="post" action="/studio/refinement" class="actions">
14
+ <%== csrf_tag %>
15
+ <label>
16
+ Agent
17
+ <select name="agent">
18
+ <% @agents.each do |id| %>
19
+ <option value="<%= id %>"<%== ' selected' if id == @agent %>><%= id %></option>
20
+ <% end %>
21
+ </select>
22
+ </label>
23
+ <label title="Ignore the previous run and read the full window instead of only what happened since.">
24
+ <input type="checkbox" name="full" value="1"> full window
25
+ </label>
26
+ <button type="submit" class="primary">Run</button>
27
+ </form>
28
+ <div class="skill-desc muted">
29
+ A run reads this agent's own tasks, transcripts and tool traces and records what
30
+ broke, ranked. It never edits the agent and never calls a model.
31
+ </div>
32
+ </div>
33
+
34
+ <% if @agent && @agents.size > 1 %>
35
+ <div class="filter-bar">
36
+ <% @agents.each do |id| %>
37
+ <a class="btn" href="/studio/refinement?agent=<%= Rack::Utils.escape(id) %>"<%== ' aria-current="page"' if id == @agent %>><%= id %></a>
38
+ <% end %>
39
+ </div>
40
+ <% end %>
41
+
42
+ <%# RFC-0013 §3.6 — the review IS the product. A gated proposal shows the exact
43
+ edit, what it claims to fix, and how it scored against the accepted baseline,
44
+ because approving is a decision and a decision needs those three things. %>
45
+ <% if @proposal %>
46
+ <h2>Proposal<span class="sub">gated · awaiting your approval</span></h2>
47
+ <div class="card">
48
+ <% cand = @proposal.candidate || {} %>
49
+ <div class="skill-desc">
50
+ <span class="pill ok">gate passed</span>
51
+ <span class="mono"><%= @proposal.gate["passed_cases"] %>/<%= @proposal.gate["cases"] %> case(s)</span>
52
+ <span class="muted">no regression against <%= @proposal.gate["baseline_cases"] %> baselined case(s)</span>
53
+ <span class="muted">· proposed by <span class="mono"><%= cand["proposer"] %></span></span>
54
+ <%# RFC-0013 §3.9 — when a PANEL ran, the operator is looking at the winner of
55
+ several, and which ones it beat is part of the decision. %>
56
+ <% if @proposal.panel.size > 1 %>
57
+ <span class="muted">· best of <%= @proposal.panel.size %> candidate(s)</span>
58
+ <% end %>
59
+ <% if @proposal.cost && @proposal.cost["spent"].to_i.positive? %>
60
+ <span class="muted">· <%= @proposal.cost["spent"] %> tokens<% if @proposal.cost["cached"].to_i.positive? %>
61
+ (<%= @proposal.cost["cached"] %> cached)<% end %></span>
62
+ <% end %>
63
+ </div>
64
+ <% if presence(cand["rationale"]) %>
65
+ <div class="skill-desc muted"><%= cand["rationale"] %></div>
66
+ <% end %>
67
+
68
+ <% Array(cand["edits"]).each do |e| %>
69
+ <div class="skill-desc">
70
+ <span class="pill"><%= e["op"] %></span>
71
+ <span class="mono"><%= e["file"] %></span>
72
+ <% if presence(e["anchor"]) %><span class="muted"><%= e["anchor"] %></span><% end %>
73
+ <% if Array(e["addresses"]).any? %>
74
+ <span class="muted">· addresses <%= Array(e["addresses"]).join(", ") %></span>
75
+ <% end %>
76
+ </div>
77
+ <pre class="diff"><% if e["op"] == "replace" %><span class="del">- <%= e["before"] %></span>
78
+ <% end %><span class="ins">+ <%= e["after"] %></span></pre>
79
+ <% end %>
80
+
81
+ <form method="post" action="/studio/refinement/resolve" class="actions">
82
+ <%== csrf_tag %>
83
+ <input type="hidden" name="agent" value="<%= @agent %>">
84
+ <input type="hidden" name="run_id" value="<%= @proposal.id %>">
85
+ <label>
86
+ Note
87
+ <input type="text" name="note" placeholder="why (optional)">
88
+ </label>
89
+ <button type="submit" class="primary" name="decision" value="approved">Approve &amp; apply</button>
90
+ <button type="submit" class="ghost" name="decision" value="rejected">Reject</button>
91
+ </form>
92
+ <div class="skill-desc muted">
93
+ Approving writes the edits to the agent's files and versions the previous
94
+ content — undo it any time from the file's History.
95
+ </div>
96
+ </div>
97
+
98
+ <%# The candidates that LOST. Shown because the panel's value is comparative: an
99
+ operator deciding whether the loop earns its keep needs to see that three
100
+ models were asked and what the other two wrote. %>
101
+ <% losers = @proposal.panel.reject { |e| (e["candidate"] || {})["id"] == cand["id"] } %>
102
+ <% if losers.any? %>
103
+ <h2>Also proposed<span class="sub">scored and not chosen</span></h2>
104
+ <div class="card">
105
+ <ul class="history">
106
+ <% losers.each do |entry| %>
107
+ <% other = entry["candidate"] || {}; verdict = entry["gate"] || {} %>
108
+ <li>
109
+ <span class="pill <%= verdict["passed"] ? 'ok' : 'err' %>"><%= verdict["passed"] ? "passed" : "no" %></span>
110
+ <span class="mono"><%= Array(entry["proposers"]).join(" + ") %></span>
111
+ <span class="preview muted">
112
+ <%= Array(other["edits"]).size %> edit(s)
113
+ <% if verdict["cases"].to_i.positive? %>· <%= verdict["passed_cases"] %>/<%= verdict["cases"] %> case(s)<% end %>
114
+ <% if presence(verdict["reason"]) %>· <%= verdict["reason"] %><% end %>
115
+ </span>
116
+ </li>
117
+ <% end %>
118
+ </ul>
119
+ </div>
120
+ <% end %>
121
+ <% end %>
122
+
123
+ <% if @run.nil? %>
124
+ <div class="empty">No run yet for <span class="mono"><%= @agent %></span>. Press Run to read the last conversations.</div>
125
+ <% else %>
126
+ <h2>Findings<span class="sub"><%= @run.findings.size %> · <%= @run.status %><% if @run.excluded.to_i.positive? %> · <%= @run.excluded %> turn(s) excluded<% end %> · <%= @run.finished_at || @run.started_at %></span></h2>
127
+
128
+ <%# RFC-0013 §3.4 — the model writes the candidate, the gate scores it. Offered on
129
+ a finished report and only when nothing is already awaiting an answer: a second
130
+ proposal while one is pending would ask the operator to review two edits to the
131
+ same file that were never scored together. %>
132
+ <% if @proposal.nil? && @run.status == :completed && @run.findings.any? %>
133
+ <div class="card">
134
+ <form method="post" action="/studio/refinement/propose" class="actions">
135
+ <%== csrf_tag %>
136
+ <input type="hidden" name="agent" value="<%= @agent %>">
137
+ <input type="hidden" name="run_id" value="<%= @run.id %>">
138
+ <button type="submit" class="primary">Propose a fix</button>
139
+ </form>
140
+ <div class="skill-desc muted">
141
+ Asks this agent's configured proposer(s) for a small, anchored edit to its
142
+ allowlisted files, then scores each one by replaying the golden set against a
143
+ throwaway clone and keeps the best. Nothing is written until you approve it.
144
+ Takes a while and costs provider calls — the replay is a real conversation per
145
+ case, per candidate.
146
+ </div>
147
+ </div>
148
+ <% end %>
149
+
150
+ <%# RFC-0013 §3.6 — `mode: auto_apply` changed this agent's files with nobody in
151
+ the loop, so the page owes the operator exactly three things: what changed, why,
152
+ and where to undo it. The write versioned, so History IS the undo. %>
153
+ <% if @run.status == :applied && (@run.decision || {})["by"] == "auto_apply" %>
154
+ <div class="card">
155
+ <div class="skill-desc">
156
+ <span class="pill ok">auto-applied</span>
157
+ <span class="muted"><%= (@run.decision || {})["note"] %> · <%= (@run.decision || {})["at"] %></span>
158
+ </div>
159
+ <% Array((@run.candidate || {})["edits"]).each do |e| %>
160
+ <div class="skill-desc">
161
+ <span class="pill"><%= e["op"] %></span>
162
+ <a class="mono" href="/studio/agents/<%= Rack::Utils.escape(@agent) %>?file=<%= Rack::Utils.escape(e["file"]) %>"><%= e["file"] %></a>
163
+ </div>
164
+ <pre class="diff"><% if e["op"] == "replace" %><span class="del">- <%= e["before"] %></span>
165
+ <% end %><span class="ins">+ <%= e["after"] %></span></pre>
166
+ <% end %>
167
+ <div class="skill-desc muted">
168
+ Undo it from the file's History — the previous content is one click away.
169
+ </div>
170
+ </div>
171
+ <% end %>
172
+
173
+ <%# A gate that refused is the most useful thing on this page and the easiest to
174
+ lose: without it, pressing Propose and getting nothing back reads as a bug. %>
175
+ <% if @run.status == :rejected && @run.gate.is_a?(Hash) && !@run.gate_passed? %>
176
+ <div class="card">
177
+ <div class="skill-desc">
178
+ <span class="pill err">gate refused</span>
179
+ <span class="mono"><%= @run.gate["reason"] %></span>
180
+ </div>
181
+ <% Array((@run.candidate || {})["dropped"]).each do |d| %>
182
+ <div class="skill-desc muted">dropped <span class="mono"><%= d["file"] %></span> — <%= d["reason"] %></div>
183
+ <% end %>
184
+ </div>
185
+ <% end %>
186
+
187
+ <% if @run.status == :failed %>
188
+ <div class="empty">This run failed: <span class="mono"><%= @run.error %></span></div>
189
+ <% elsif @run.findings.empty? %>
190
+ <div class="empty">Nothing found in this window. Either traffic was clean, or the window was empty.</div>
191
+ <% else %>
192
+ <div class="card">
193
+ <ul class="history">
194
+ <% @run.findings.each do |f| %>
195
+ <li>
196
+ <span class="pill"><%= f["kind"] %></span>
197
+ <span class="mono">×<%= f["count"] %></span>
198
+ <span class="preview"><%= f["title"] %></span>
199
+ <% if f["detail"] %><div class="skill-desc muted"><%= f["detail"] %></div><% end %>
200
+ <% if Array(f["sessions"]).any? %>
201
+ <div class="skill-desc muted">
202
+ <% Array(f["sessions"]).each do |sid| %>
203
+ <a class="mono" href="/studio/sessions/<%= Rack::Utils.escape(sid) %>"><%= sid[0, 12] %></a>
204
+ <% end %>
205
+ </div>
206
+ <% end %>
207
+ </li>
208
+ <% end %>
209
+ </ul>
210
+ </div>
211
+ <% end %>
212
+ <% end %>
213
+
214
+ <% if @runs.any? %>
215
+ <h2>Runs<span class="sub">most recent first</span></h2>
216
+ <div class="card">
217
+ <ul class="history">
218
+ <% @runs.each do |run| %>
219
+ <li>
220
+ <span class="pill <%= run.status == :failed ? 'err' : 'ok' %>"><%= run.status %></span>
221
+ <span class="mono"><%= run.id[0, 12] %></span>
222
+ <span class="preview muted">
223
+ <%= run.findings.size %> finding(s) ·
224
+ window <%= refinement_window(run.window) %> ·
225
+ <% if run.panel.size > 1 %><%= run.panel.size %> candidate(s) · <% end %>
226
+ <% if run.cost && run.cost["spent"].to_i.positive? %><%= run.cost["spent"] %> tokens · <% end %>
227
+ <%= run.started_at %>
228
+ </span>
229
+ </li>
230
+ <% end %>
231
+ </ul>
232
+ </div>
233
+ <% end %>
234
+ <% end %>
@@ -0,0 +1,62 @@
1
+ <header class="page-head">
2
+ <div>
3
+ <nav class="crumbs"><a href="/studio/chats">chats</a> / <span>session</span></nav>
4
+ <h1>Session
5
+ <span class="copy-id" data-controller="clipboard" data-clipboard-text-value="<%= @session.id %>">
6
+ <span class="mono"><%= @session.id[0, 16] %></span>
7
+ <button type="button" class="copy-btn" data-clipboard-target="button" data-action="clipboard#copy" aria-label="Copy full session id">copy id</button>
8
+ </span>
9
+ <span class="sub"><%= Array(@session.messages).size %> message(s) · updated <%= @session.updated_at %></span>
10
+ </h1>
11
+ </div>
12
+ <a class="btn" href="/studio/playground?session_id=<%= Rack::Utils.escape(@session.id) %>">Continue in playground →</a>
13
+ </header>
14
+
15
+ <% traces = @tool_traces || {} %>
16
+ <% unless traces.empty? %>
17
+ <div class="card tooltrace">
18
+ <div class="trace-head">
19
+ <h2>Tool calls</h2>
20
+ <span class="muted">debug — args + response, masked</span>
21
+ </div>
22
+ <% traces.keys.sort.each do |turn| %>
23
+ <div class="trace-turn">
24
+ <div class="turn-label">turn <%= turn %></div>
25
+ <% traces[turn].each do |t| %>
26
+ <details class="trace-item <%= t["ok"] ? "ok" : "err" %>">
27
+ <summary>
28
+ <span class="badge"><%= t["ok"] ? "ok" : "error" %></span>
29
+ <strong><%= t["tool"] %></strong>
30
+ <span class="lat"><%= t["ms"] %>ms</span>
31
+ <span class="at"><%= t["at"] %></span>
32
+ </summary>
33
+ <div class="kv"><span>args</span>
34
+ <div class="copywrap" data-controller="clipboard">
35
+ <button type="button" class="copy-btn" data-clipboard-target="button" data-action="clipboard#copy">copy</button>
36
+ <pre data-clipboard-target="source"><%= t["args"] %></pre>
37
+ </div>
38
+ </div>
39
+ <div class="kv"><span>response</span>
40
+ <div class="copywrap" data-controller="clipboard">
41
+ <button type="button" class="copy-btn" data-clipboard-target="button" data-action="clipboard#copy">copy</button>
42
+ <pre data-clipboard-target="source"><%= t["result"] %></pre>
43
+ </div>
44
+ </div>
45
+ </details>
46
+ <% end %>
47
+ </div>
48
+ <% end %>
49
+ </div>
50
+ <% end %>
51
+
52
+ <div class="card transcript">
53
+ <% if Array(@session.messages).empty? %>
54
+ <div class="empty">This session has no messages.</div>
55
+ <% else %>
56
+ <div class="thread">
57
+ <%# `prev` is what makes a STEERED message visible: a customer message that lands
58
+ right after a tool result arrived mid-run (RFC-0015). See _message.erb. %>
59
+ <% Array(@session.messages).each_with_index do |m, i| %><%== render("_message", locals: { m: m, prev: i.positive? ? @session.messages[i - 1] : nil }) %><% end %>
60
+ </div>
61
+ <% end %>
62
+ </div>