insika 0.0.1 → 0.2.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 (277) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +361 -0
  3. data/LICENSE +21 -0
  4. data/README.md +136 -2
  5. data/bin/insika +366 -0
  6. data/docs/AGENTS.md +618 -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 +117 -0
  11. data/docs/DEPLOY.md +354 -0
  12. data/docs/EMBEDDING.md +198 -0
  13. data/docs/EVALS.md +273 -0
  14. data/docs/LOADTEST.md +232 -0
  15. data/docs/OBSERVABILITY.md +374 -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 +375 -0
  22. data/docs/SKILLS.md +284 -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 +255 -0
  34. data/lib/insika/alert_dispatcher.rb +139 -0
  35. data/lib/insika/allowlist.rb +28 -0
  36. data/lib/insika/baseline_store.rb +74 -0
  37. data/lib/insika/budget_ledger.rb +135 -0
  38. data/lib/insika/capability/resolved_tool.rb +34 -0
  39. data/lib/insika/capability_registry.rb +112 -0
  40. data/lib/insika/channel_delivery.rb +153 -0
  41. data/lib/insika/channel_registry.rb +30 -0
  42. data/lib/insika/channels/relay.rb +178 -0
  43. data/lib/insika/channels/web/widget.js +283 -0
  44. data/lib/insika/channels/web.rb +211 -0
  45. data/lib/insika/channels/webhook.rb +58 -0
  46. data/lib/insika/chat_builder.rb +303 -0
  47. data/lib/insika/checkpoint.rb +13 -0
  48. data/lib/insika/checkpoint_store.rb +153 -0
  49. data/lib/insika/circuit_state.rb +114 -0
  50. data/lib/insika/coercion.rb +58 -0
  51. data/lib/insika/command.rb +32 -0
  52. data/lib/insika/command_bus.rb +39 -0
  53. data/lib/insika/commands/agent_payload.rb +43 -0
  54. data/lib/insika/commands/approve_action.rb +46 -0
  55. data/lib/insika/commands/cancel_task.rb +33 -0
  56. data/lib/insika/commands/create_agent.rb +54 -0
  57. data/lib/insika/commands/create_session.rb +67 -0
  58. data/lib/insika/commands/delete_agent.rb +33 -0
  59. data/lib/insika/commands/delete_agent_file.rb +50 -0
  60. data/lib/insika/commands/delete_data_tool.rb +33 -0
  61. data/lib/insika/commands/delete_llm_provider.rb +36 -0
  62. data/lib/insika/commands/delete_mcp.rb +30 -0
  63. data/lib/insika/commands/delete_skill.rb +43 -0
  64. data/lib/insika/commands/delete_system_file.rb +29 -0
  65. data/lib/insika/commands/gate_refinement.rb +245 -0
  66. data/lib/insika/commands/import_mcp_tools.rb +48 -0
  67. data/lib/insika/commands/import_tools.rb +81 -0
  68. data/lib/insika/commands/issue_tenant_token.rb +41 -0
  69. data/lib/insika/commands/memory_add_note.rb +32 -0
  70. data/lib/insika/commands/memory_forget_fact.rb +32 -0
  71. data/lib/insika/commands/memory_put_fact.rb +35 -0
  72. data/lib/insika/commands/pause_task.rb +29 -0
  73. data/lib/insika/commands/resolve_refinement.rb +126 -0
  74. data/lib/insika/commands/restore_agent_file.rb +36 -0
  75. data/lib/insika/commands/restore_data_tool.rb +34 -0
  76. data/lib/insika/commands/restore_system_file.rb +31 -0
  77. data/lib/insika/commands/resume_task.rb +85 -0
  78. data/lib/insika/commands/revoke_token.rb +39 -0
  79. data/lib/insika/commands/rotate_tenant_token.rb +43 -0
  80. data/lib/insika/commands/run_refinement.rb +133 -0
  81. data/lib/insika/commands/send_message.rb +150 -0
  82. data/lib/insika/commands/set_agent_tools.rb +39 -0
  83. data/lib/insika/commands/set_skill_agents.rb +112 -0
  84. data/lib/insika/commands/trigger_workflow.rb +80 -0
  85. data/lib/insika/commands/update_agent.rb +49 -0
  86. data/lib/insika/commands/update_settings.rb +33 -0
  87. data/lib/insika/commands/upsert_llm_provider.rb +34 -0
  88. data/lib/insika/commands/upsert_mcp.rb +32 -0
  89. data/lib/insika/commands/write_agent_file.rb +57 -0
  90. data/lib/insika/commands/write_data_tool.rb +43 -0
  91. data/lib/insika/commands/write_golden.rb +58 -0
  92. data/lib/insika/commands/write_skill.rb +60 -0
  93. data/lib/insika/commands/write_system_file.rb +31 -0
  94. data/lib/insika/config_store.rb +89 -0
  95. data/lib/insika/context/builder.rb +166 -0
  96. data/lib/insika/context/catalog_provider.rb +23 -0
  97. data/lib/insika/context/fragment.rb +43 -0
  98. data/lib/insika/context/priority.rb +30 -0
  99. data/lib/insika/context/provider.rb +19 -0
  100. data/lib/insika/context/providers/memory.rb +60 -0
  101. data/lib/insika/context/providers/prompt.rb +105 -0
  102. data/lib/insika/context/providers/request.rb +32 -0
  103. data/lib/insika/context/providers/session.rb +123 -0
  104. data/lib/insika/context/providers/skill.rb +24 -0
  105. data/lib/insika/context/providers/skill_trigger.rb +128 -0
  106. data/lib/insika/context/providers/tool_search.rb +20 -0
  107. data/lib/insika/context_trace_store.rb +92 -0
  108. data/lib/insika/delegation_store.rb +153 -0
  109. data/lib/insika/doctor.rb +539 -0
  110. data/lib/insika/dsl/definition.rb +55 -0
  111. data/lib/insika/dsl/runtime.rb +382 -0
  112. data/lib/insika/dsl/server_boot.rb +98 -0
  113. data/lib/insika/dsl/system.rb +93 -0
  114. data/lib/insika/dsl/workflow_adapter.rb +59 -0
  115. data/lib/insika/dsl.rb +364 -0
  116. data/lib/insika/edge_limiter.rb +268 -0
  117. data/lib/insika/egress_guard.rb +75 -0
  118. data/lib/insika/env_schema.rb +249 -0
  119. data/lib/insika/errors.rb +201 -0
  120. data/lib/insika/evals/assertions.rb +247 -0
  121. data/lib/insika/evals/baseline.rb +69 -0
  122. data/lib/insika/evals/golden.rb +172 -0
  123. data/lib/insika/evals/judge.rb +225 -0
  124. data/lib/insika/evals/pairwise.rb +178 -0
  125. data/lib/insika/evals/report.rb +115 -0
  126. data/lib/insika/evals/runner.rb +141 -0
  127. data/lib/insika/evals/transport.rb +178 -0
  128. data/lib/insika/event.rb +18 -0
  129. data/lib/insika/event_stream.rb +132 -0
  130. data/lib/insika/executor.rb +1995 -0
  131. data/lib/insika/frontmatter.rb +42 -0
  132. data/lib/insika/golden_store.rb +145 -0
  133. data/lib/insika/hooks.rb +48 -0
  134. data/lib/insika/http_client.rb +63 -0
  135. data/lib/insika/inbound_log.rb +84 -0
  136. data/lib/insika/llm_configurator.rb +99 -0
  137. data/lib/insika/llm_provider_store.rb +83 -0
  138. data/lib/insika/loop_detector.rb +143 -0
  139. data/lib/insika/mcp_http_client.rb +67 -0
  140. data/lib/insika/mcp_store.rb +115 -0
  141. data/lib/insika/mcp_tool_ingestor.rb +143 -0
  142. data/lib/insika/memory_store.rb +93 -0
  143. data/lib/insika/message_origin.rb +76 -0
  144. data/lib/insika/middleware.rb +36 -0
  145. data/lib/insika/model_policy.rb +52 -0
  146. data/lib/insika/model_resolver.rb +176 -0
  147. data/lib/insika/model_selection.rb +115 -0
  148. data/lib/insika/onboarding.rb +208 -0
  149. data/lib/insika/outbox_store.rb +166 -0
  150. data/lib/insika/overlay_tool_registry.rb +102 -0
  151. data/lib/insika/pack.rb +102 -0
  152. data/lib/insika/pack_importer.rb +123 -0
  153. data/lib/insika/pending_action_store.rb +120 -0
  154. data/lib/insika/plugin/loader.rb +356 -0
  155. data/lib/insika/plugin.rb +35 -0
  156. data/lib/insika/policy/engine.rb +83 -0
  157. data/lib/insika/policy/policy.rb +120 -0
  158. data/lib/insika/policy_registry.rb +23 -0
  159. data/lib/insika/profile_source.rb +143 -0
  160. data/lib/insika/prompt_catalog.rb +61 -0
  161. data/lib/insika/provider_error_classifier.rb +160 -0
  162. data/lib/insika/queue_policy.rb +167 -0
  163. data/lib/insika/recovery.rb +168 -0
  164. data/lib/insika/refinement/candidate.rb +159 -0
  165. data/lib/insika/refinement/evidence_collector.rb +371 -0
  166. data/lib/insika/refinement/gate.rb +234 -0
  167. data/lib/insika/refinement/panel.rb +222 -0
  168. data/lib/insika/refinement/proposer.rb +262 -0
  169. data/lib/insika/refinement_store.rb +295 -0
  170. data/lib/insika/registry.rb +59 -0
  171. data/lib/insika/reliability.rb +185 -0
  172. data/lib/insika/safety/config.rb +109 -0
  173. data/lib/insika/safety/detectors.rb +176 -0
  174. data/lib/insika/safety/factory.rb +102 -0
  175. data/lib/insika/safety/input_guardrail.rb +102 -0
  176. data/lib/insika/safety/moderator.rb +94 -0
  177. data/lib/insika/safety/output_filter.rb +79 -0
  178. data/lib/insika/safety/output_validator.rb +101 -0
  179. data/lib/insika/safety/safe_responses.rb +47 -0
  180. data/lib/insika/sandbox/boundary.rb +93 -0
  181. data/lib/insika/sandbox/docker.rb +74 -0
  182. data/lib/insika/sandbox/local.rb +33 -0
  183. data/lib/insika/sandbox/runner.rb +80 -0
  184. data/lib/insika/sandbox.rb +85 -0
  185. data/lib/insika/schema_guard.rb +147 -0
  186. data/lib/insika/secret_masking.rb +34 -0
  187. data/lib/insika/server/a2a/agent_card.rb +27 -0
  188. data/lib/insika/server/a2a/app.rb +112 -0
  189. data/lib/insika/server/a2a/client.rb +101 -0
  190. data/lib/insika/server/a2a/errors.rb +32 -0
  191. data/lib/insika/server/a2a/http.rb +42 -0
  192. data/lib/insika/server/a2a/message.rb +27 -0
  193. data/lib/insika/server/a2a/protocol.rb +45 -0
  194. data/lib/insika/server/a2a/remotes.rb +25 -0
  195. data/lib/insika/server/a2a/task_projection.rb +40 -0
  196. data/lib/insika/server/app.rb +1022 -0
  197. data/lib/insika/server/boot.rb +119 -0
  198. data/lib/insika/server/rack_app.rb +118 -0
  199. data/lib/insika/server/responses.rb +165 -0
  200. data/lib/insika/server/sse_body.rb +96 -0
  201. data/lib/insika/server/tenant_auth.rb +61 -0
  202. data/lib/insika/session_actor.rb +162 -0
  203. data/lib/insika/session_store.rb +143 -0
  204. data/lib/insika/settings_store.rb +154 -0
  205. data/lib/insika/shutdown.rb +125 -0
  206. data/lib/insika/skill_catalog.rb +220 -0
  207. data/lib/insika/skill_store.rb +127 -0
  208. data/lib/insika/steer_injector.rb +110 -0
  209. data/lib/insika/store.rb +52 -0
  210. data/lib/insika/stores/memory.rb +123 -0
  211. data/lib/insika/stores/sqlite.rb +183 -0
  212. data/lib/insika/studio/app.rb +1693 -0
  213. data/lib/insika/studio/assets/dist/application.css +1 -0
  214. data/lib/insika/studio/assets/dist/application.js +70 -0
  215. data/lib/insika/studio/forms.rb +335 -0
  216. data/lib/insika/studio/nav_icons.rb +31 -0
  217. data/lib/insika/studio/views/_message.erb +44 -0
  218. data/lib/insika/studio/views/agent_detail.erb +285 -0
  219. data/lib/insika/studio/views/agents.erb +63 -0
  220. data/lib/insika/studio/views/approvals.erb +41 -0
  221. data/lib/insika/studio/views/chats.erb +34 -0
  222. data/lib/insika/studio/views/evals.erb +83 -0
  223. data/lib/insika/studio/views/home.erb +72 -0
  224. data/lib/insika/studio/views/layout.erb +94 -0
  225. data/lib/insika/studio/views/login.erb +17 -0
  226. data/lib/insika/studio/views/mcp.erb +91 -0
  227. data/lib/insika/studio/views/not_found.erb +5 -0
  228. data/lib/insika/studio/views/playground.erb +47 -0
  229. data/lib/insika/studio/views/refinement.erb +234 -0
  230. data/lib/insika/studio/views/session.erb +137 -0
  231. data/lib/insika/studio/views/settings.erb +168 -0
  232. data/lib/insika/studio/views/skills.erb +141 -0
  233. data/lib/insika/studio/views/system_files.erb +65 -0
  234. data/lib/insika/studio/views/task.erb +105 -0
  235. data/lib/insika/studio/views/tasks.erb +33 -0
  236. data/lib/insika/studio/views/tool_edit.erb +107 -0
  237. data/lib/insika/studio/views/tools.erb +89 -0
  238. data/lib/insika/subagent_graph.rb +96 -0
  239. data/lib/insika/system_file_store.rb +96 -0
  240. data/lib/insika/task_actor.rb +128 -0
  241. data/lib/insika/task_store.rb +250 -0
  242. data/lib/insika/telemetry/pricing.rb +104 -0
  243. data/lib/insika/telemetry/recorder.rb +228 -0
  244. data/lib/insika/telemetry.rb +127 -0
  245. data/lib/insika/testing/store_contract.rb +270 -0
  246. data/lib/insika/tick.rb +122 -0
  247. data/lib/insika/token_estimator.rb +16 -0
  248. data/lib/insika/token_store.rb +168 -0
  249. data/lib/insika/tool_assembly.rb +140 -0
  250. data/lib/insika/tool_catalog.rb +89 -0
  251. data/lib/insika/tool_definition.rb +518 -0
  252. data/lib/insika/tool_envelope.rb +140 -0
  253. data/lib/insika/tool_manifest.rb +218 -0
  254. data/lib/insika/tool_output_compressor.rb +100 -0
  255. data/lib/insika/tool_registry.rb +21 -0
  256. data/lib/insika/tool_store.rb +135 -0
  257. data/lib/insika/tool_trace_store.rb +92 -0
  258. data/lib/insika/tools/a2a_remote.rb +48 -0
  259. data/lib/insika/tools/agent_enum.rb +68 -0
  260. data/lib/insika/tools/concurrency.rb +54 -0
  261. data/lib/insika/tools/data_defined_tool.rb +219 -0
  262. data/lib/insika/tools/load_skill.rb +99 -0
  263. data/lib/insika/tools/remember.rb +53 -0
  264. data/lib/insika/tools/stuck_signal.rb +44 -0
  265. data/lib/insika/tools/subagent.rb +75 -0
  266. data/lib/insika/tools/subagents.rb +77 -0
  267. data/lib/insika/tools/tool_search.rb +94 -0
  268. data/lib/insika/turn_output.rb +139 -0
  269. data/lib/insika/turn_state.rb +162 -0
  270. data/lib/insika/turn_timing.rb +56 -0
  271. data/lib/insika/usage_ledger.rb +47 -0
  272. data/lib/insika/version.rb +3 -1
  273. data/lib/insika/wiring/graph.rb +249 -0
  274. data/lib/insika/workflow.rb +185 -0
  275. data/lib/insika/workflow_registry.rb +33 -0
  276. data/lib/insika.rb +220 -4
  277. metadata +412 -8
@@ -0,0 +1,285 @@
1
+ <header class="page-head">
2
+ <div>
3
+ <nav class="crumbs"><a href="/studio/agents">agents</a> / <span><%= @agent.id %></span></nav>
4
+ <h1><span class="avatar sm"><%= @agent.id[0, 2].upcase %></span><%= @agent.id %>
5
+ <span class="sub"><%= @agent.model %><%== " · #{h(@agent.provider)}" if @agent.provider %></span>
6
+ </h1>
7
+ </div>
8
+ <a class="btn primary" href="/studio/playground?agent=<%= Rack::Utils.escape(@agent.id) %>">Open playground →</a>
9
+ </header>
10
+
11
+ <nav class="subnav">
12
+ <a href="#config">config</a>
13
+ <a href="#prompts">prompts</a>
14
+ <a href="#skills">skills</a>
15
+ <a href="#memory">memory</a>
16
+ <a href="#history">history</a>
17
+ </nav>
18
+
19
+ <!-- ── Config / model ────────────────────────────────────────────────────── -->
20
+ <section class="section" id="config">
21
+ <div class="section-head"><h2>Configuration</h2><p class="muted">model, provider, memory, limits, generation params and the model fence — saved via <code>update_agent</code>.</p></div>
22
+ <form class="card" method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/config">
23
+ <%== csrf_tag %>
24
+ <div class="grid-2">
25
+ <label>model <span class="muted">(blank = platform <code>default_model</code>)</span>
26
+ <input type="text" name="model" value="<%= @agent.model %>" placeholder="inherit platform default">
27
+ </label>
28
+ <label>provider
29
+ <input type="text" name="provider" value="<%= @agent.provider %>" placeholder="deepseek">
30
+ </label>
31
+ <label>turn_timeout (s)
32
+ <input type="text" name="turn_timeout" value="<%= @agent.limits[:turn_timeout] %>" inputmode="numeric">
33
+ </label>
34
+ <label>tool_timeout (s)
35
+ <input type="text" name="tool_timeout" value="<%= @agent.limits[:tool_timeout] %>" inputmode="numeric">
36
+ </label>
37
+ <label>tool_concurrency <span class="muted">(parallel tool calls; 1 = serial, off for turns with approvals)</span>
38
+ <input type="text" name="tool_concurrency" value="<%= @agent.limits[:tool_concurrency] %>" inputmode="numeric">
39
+ </label>
40
+ <label>chat_rate_limit <span class="muted">(turns/chat/window; blank = platform, 0 = off)</span>
41
+ <input type="text" name="chat_rate_limit" value="<%= @agent.limits[:chat_rate_limit] %>" inputmode="numeric" placeholder="inherit platform edge">
42
+ </label>
43
+ <label>agent_token_ceiling <span class="muted">(tokens/window; blank = platform, 0 = off)</span>
44
+ <input type="text" name="agent_token_ceiling" value="<%= @agent.limits[:agent_token_ceiling] %>" inputmode="numeric" placeholder="inherit platform edge">
45
+ </label>
46
+ </div>
47
+ <label class="check">
48
+ <input type="checkbox" name="memory" value="1"<%== " checked" if @agent.memory %>>
49
+ cross-session memory (the BIA remembers across conversations)
50
+ </label>
51
+
52
+ <fieldset class="subgroup">
53
+ <legend>generation params <span class="muted">— blank = provider default</span></legend>
54
+ <div class="grid-3">
55
+ <label>temperature
56
+ <input type="text" name="temperature" value="<%= agent_param(@params, :temperature) %>" inputmode="decimal" placeholder="0.7">
57
+ </label>
58
+ <label>max_tokens
59
+ <input type="text" name="max_tokens" value="<%= agent_param(@params, :max_tokens) %>" inputmode="numeric" placeholder="4096">
60
+ </label>
61
+ <label>reasoning <span class="muted">(off/on/effort — blank = inherit model/platform)</span>
62
+ <%== thinking_select("thinking", agent_param(@params, :thinking), "inherit") %>
63
+ </label>
64
+ </div>
65
+ </fieldset>
66
+
67
+ <label>model fence <span class="muted">(<code>model_policy.allow</code> — one ref per line: <code>provider/model</code>, <code>provider/*</code> or <code>model</code>; blank = no fence)</span>
68
+ <textarea name="model_policy_allow" rows="3" placeholder="deepseek/*&#10;openai/gpt-4o-mini"><%= @model_policy_allow %></textarea>
69
+ </label>
70
+
71
+ <fieldset class="subgroup">
72
+ <legend>guardrails <span class="muted">— content safety</span></legend>
73
+ <label class="check">
74
+ <input type="checkbox" name="guardrail_input" value="1"<%== " checked" if guardrail_field(@guardrails, :input, true) %>>
75
+ input guardrail <span class="muted">— block injection / abuse / inappropriate before the LLM</span>
76
+ </label>
77
+ <label class="check">
78
+ <input type="checkbox" name="guardrail_output" value="1"<%== " checked" if guardrail_field(@guardrails, :output, true) %>>
79
+ output guardrail <span class="muted">— redact PII/secret in the stream + flag policy slips</span>
80
+ </label>
81
+ <div class="grid-2">
82
+ <label>strictness
83
+ <% strictness = guardrail_field(@guardrails, :strictness, "medium").to_s %>
84
+ <select name="guardrail_strictness">
85
+ <% %w[low medium high].each do |lvl| %>
86
+ <option value="<%= lvl %>"<%== " selected" if lvl == strictness %>><%= lvl %></option>
87
+ <% end %>
88
+ </select>
89
+ </label>
90
+ <label>LLM moderator <span class="muted">(model ref, or <code>on</code> for the platform utility_model; blank = deterministic only)</span>
91
+ <input type="text" name="guardrail_moderator" value="<%= guardrail_field(@guardrails, :moderator, "") %>" placeholder="deepseek/deepseek-v4-flash">
92
+ </label>
93
+ </div>
94
+ <details class="newfile">
95
+ <summary>safe replies — override per category <span class="muted">(blank = engine default; this agent's voice/language)</span></summary>
96
+ <% gr_resp = (@guardrails["responses"] || @guardrails[:responses] || {}) %>
97
+ <label>default <span class="muted">(catch-all for any blocked category)</span>
98
+ <textarea name="guardrail_response_default" rows="2" placeholder="I can't help with that particular request…"><%= guardrail_field(gr_resp, :default, "") %></textarea>
99
+ </label>
100
+ <div class="grid-2">
101
+ <% %w[injection sexual abuse escalate].each do |cat| %>
102
+ <label><%= cat %>
103
+ <textarea name="guardrail_response_<%= cat %>" rows="2"><%= guardrail_field(gr_resp, cat, "") %></textarea>
104
+ </label>
105
+ <% end %>
106
+ </div>
107
+ </details>
108
+ </fieldset>
109
+
110
+ <div class="actions"><button type="submit" class="primary" data-turbo-submits-with="Saving…">Save config</button></div>
111
+ </form>
112
+ </section>
113
+
114
+ <!-- ── Prompts / identity (island code-editor) ───────────────────────────── -->
115
+ <section class="section" id="prompts">
116
+ <div class="section-head">
117
+ <h2>Prompts <span class="pill info plain"><%= @prompt_files.size %></span></h2>
118
+ <p class="muted">the BIA's own identity (store-backed, versioned). Empty = uses the deployment default.</p>
119
+ </div>
120
+
121
+ <% if @prompt_files.empty? %>
122
+ <div class="empty">No authored prompts. Create a file below (e.g. <code>IDENTITY.md</code>) to give this agent its own identity.</div>
123
+ <% end %>
124
+
125
+ <% @prompt_files.each do |pf| %>
126
+ <div class="card promptfile">
127
+ <div class="promptfile-head">
128
+ <strong class="mono"><%= pf[:name] %></strong>
129
+ <form method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/prompts/delete" data-turbo-confirm="Remove prompt file &quot;<%= pf[:name] %>&quot;? This can't be undone.">
130
+ <%== csrf_tag %>
131
+ <input type="hidden" name="file" value="<%= pf[:name] %>">
132
+ <button type="submit" class="ghost danger" data-turbo-submits-with="Removing…">Remove</button>
133
+ </form>
134
+ </div>
135
+ <form method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/prompts" data-controller="dirty-guard">
136
+ <%== csrf_tag %>
137
+ <input type="hidden" name="file" value="<%= pf[:name] %>">
138
+ <div data-controller="code-editor" data-code-editor-language-value="markdown" data-code-editor-preview-value="true">
139
+ <textarea name="content" data-code-editor-target="source" rows="10"><%= pf[:content] %></textarea>
140
+ </div>
141
+ <div class="actions"><button type="submit" class="primary" data-turbo-submits-with="Saving…">Save prompt</button></div>
142
+ </form>
143
+ <% unless pf[:versions].empty? %>
144
+ <details class="versions">
145
+ <summary><%= pf[:versions].size %> previous version(s)</summary>
146
+ <ul>
147
+ <% pf[:versions].each_with_index do |v, i| %>
148
+ <li>
149
+ <span class="muted mono"><%= v["at"] %></span>
150
+ <form method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/prompts/restore">
151
+ <%== csrf_tag %>
152
+ <input type="hidden" name="file" value="<%= pf[:name] %>">
153
+ <input type="hidden" name="version" value="<%= i %>">
154
+ <button type="submit" class="ghost">Restore</button>
155
+ </form>
156
+ </li>
157
+ <% end %>
158
+ </ul>
159
+ </details>
160
+ <% end %>
161
+ </div>
162
+ <% end %>
163
+
164
+ <details class="card newfile">
165
+ <summary>+ new prompt file</summary>
166
+ <form method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/prompts" data-controller="dirty-guard">
167
+ <%== csrf_tag %>
168
+ <label>file name
169
+ <input type="text" name="file" placeholder="IDENTITY.md" required>
170
+ </label>
171
+ <div data-controller="code-editor" data-code-editor-language-value="markdown" data-code-editor-preview-value="true">
172
+ <textarea name="content" data-code-editor-target="source" rows="8" placeholder="# Identity&#10;You are..."></textarea>
173
+ </div>
174
+ <div class="actions"><button type="submit" class="primary" data-turbo-submits-with="Creating…">Create prompt</button></div>
175
+ </form>
176
+ </details>
177
+ </section>
178
+
179
+ <!-- ── Agent skills ──────────────────────────────────────────────────────── -->
180
+ <section class="section" id="skills">
181
+ <div class="section-head">
182
+ <h2>Skills</h2>
183
+ <p class="muted">which catalog skills this agent may load (<code>load_skill</code>).</p>
184
+ </div>
185
+ <form class="card" method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/skills">
186
+ <%== csrf_tag %>
187
+ <label class="check">
188
+ <input type="checkbox" name="all_skills" value="1"<%== " checked" if @agent_skills.nil? %>>
189
+ all skills (open allowlist)
190
+ </label>
191
+ <% if @all_skills.empty? %>
192
+ <p class="muted">No skills in the catalog. Create one under <a href="/studio/skills/new">skills → new</a>.</p>
193
+ <% else %>
194
+ <div class="check-grid">
195
+ <% @all_skills.sort_by(&:name).each do |s| %>
196
+ <label class="check">
197
+ <input type="checkbox" name="skills[]" value="<%= s.name %>"<%== " checked" if @agent_skills&.include?(s.name) %>>
198
+ <span class="mono"><%= s.name %></span>
199
+ </label>
200
+ <% end %>
201
+ </div>
202
+ <% end %>
203
+ <div class="actions">
204
+ <button type="submit" class="primary" data-turbo-submits-with="Saving…">Save skills</button>
205
+ <a class="btn ghost" href="/studio/tools">Edit tools →</a>
206
+ </div>
207
+ </form>
208
+ </section>
209
+
210
+ <!-- ── Agent memory (tenant = id) ────────────────────────────────────────── -->
211
+ <section class="section" id="memory">
212
+ <div class="section-head">
213
+ <h2>Memory</h2>
214
+ <p class="muted">facts and notes the BIA reads each turn (tenant <code><%= @agent.id %></code>).</p>
215
+ </div>
216
+ <div class="card">
217
+ <h3>Facts</h3>
218
+ <% if @facts.empty? %>
219
+ <p class="muted">No facts.</p>
220
+ <% else %>
221
+ <ul class="kv">
222
+ <% @facts.each do |f| %>
223
+ <li>
224
+ <span class="k mono"><%= f.key %></span>
225
+ <span class="v"><%= f.value %></span>
226
+ <form method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/memory/forget" data-turbo-confirm="Forget &quot;<%= f.key %>&quot;? The agent loses this fact from the next turn on.">
227
+ <%== csrf_tag %>
228
+ <input type="hidden" name="key" value="<%= f.key %>">
229
+ <button type="submit" class="ghost danger" data-turbo-submits-with="Forgetting…">Forget</button>
230
+ </form>
231
+ </li>
232
+ <% end %>
233
+ </ul>
234
+ <% end %>
235
+ <form class="inline-form" method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/memory/fact">
236
+ <%== csrf_tag %>
237
+ <input type="text" name="key" placeholder="key (e.g. customer_name)" required>
238
+ <input type="text" name="value" placeholder="value" required>
239
+ <button type="submit" class="primary" data-turbo-submits-with="Saving…">Save fact</button>
240
+ </form>
241
+ </div>
242
+
243
+ <div class="card">
244
+ <h3>Notes <span class="muted">(recent)</span></h3>
245
+ <% if @notes.empty? %>
246
+ <p class="muted">No notes.</p>
247
+ <% else %>
248
+ <ul class="notes">
249
+ <% @notes.each do |n| %>
250
+ <li><span class="muted mono"><%= n.created_at %></span> <%= n.text %></li>
251
+ <% end %>
252
+ </ul>
253
+ <% end %>
254
+ <form class="inline-form" method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/memory/note">
255
+ <%== csrf_tag %>
256
+ <input type="text" name="text" placeholder="free-form note…" required>
257
+ <button type="submit" class="primary" data-turbo-submits-with="Adding…">Add note</button>
258
+ </form>
259
+ </div>
260
+ </section>
261
+
262
+ <!-- ── History ───────────────────────────────────────────────────────────── -->
263
+ <section class="section" id="history">
264
+ <div class="section-head">
265
+ <h2>History</h2>
266
+ <p class="muted">recent conversations (all agents — a session doesn't stamp its author).</p>
267
+ </div>
268
+ <% if @recent_sessions.empty? %>
269
+ <div class="empty">No conversations yet. Start one in the <a href="/studio/playground?agent=<%= Rack::Utils.escape(@agent.id) %>">playground</a>.</div>
270
+ <% else %>
271
+ <div class="card">
272
+ <ul class="history">
273
+ <% @recent_sessions.each do |s| %>
274
+ <li>
275
+ <a href="/studio/sessions/<%= Rack::Utils.escape(s.id) %>">
276
+ <span class="mono"><%= s.id[0, 12] %></span>
277
+ </a>
278
+ <span class="muted"><%= Array(s.messages).size %> msg · <%= s.updated_at %></span>
279
+ <% if (p = session_preview(s)) %><span class="preview muted"><%= p[0, 80] %></span><% end %>
280
+ </li>
281
+ <% end %>
282
+ </ul>
283
+ </div>
284
+ <% end %>
285
+ </section>
@@ -0,0 +1,63 @@
1
+ <header class="page-head">
2
+ <h1>Agents<span class="sub"><%= @agents.size %> configured · one BIA each</span></h1>
3
+ <a class="btn primary" href="/studio/playground">Open playground →</a>
4
+ </header>
5
+
6
+ <% if @agents.empty? %>
7
+ <div class="empty">No agents yet. Create your first BIA below (id + model), then give it an identity on its detail page.</div>
8
+ <% else %>
9
+ <div data-controller="list-filter">
10
+ <div class="filter-bar">
11
+ <input type="search" class="filter-input" placeholder="Filter agents by id, model or provider…" aria-label="Filter agents"
12
+ data-list-filter-target="query" data-action="input->list-filter#filter keydown->list-filter#clear">
13
+ <span class="filter-count mono muted" data-list-filter-target="count"></span>
14
+ </div>
15
+ <div class="agent-grid">
16
+ <% @agents.each do |a| %>
17
+ <article class="card agent" data-list-filter-target="item" data-filter-text="<%= a.id %> <%= a.model %> <%= a.provider %>">
18
+ <a class="agent-head" href="/studio/agents/<%= Rack::Utils.escape(a.id) %>">
19
+ <span class="avatar"><%= a.id[0, 2].upcase %></span>
20
+ <div>
21
+ <div class="agent-id"><%= a.id %></div>
22
+ <div class="muted mono"><%= a.model %><%== " · #{h(a.provider)}" if a.provider %></div>
23
+ </div>
24
+ <% if a.memory %><span class="pill info">memory</span><% end %>
25
+ </a>
26
+ <dl class="agent-meta">
27
+ <div><dt>tools</dt><dd><%= a.tools_allow.nil? ? "all" : a.tools_allow.size %></dd></div>
28
+ <div><dt>skills</dt><dd><%= a.skills.nil? ? "all" : Array(a.skills).size %></dd></div>
29
+ <div><dt>prompts</dt><dd><%= Array(a.prompt_files).size %></dd></div>
30
+ </dl>
31
+ <div class="agent-actions">
32
+ <a class="btn" href="/studio/agents/<%= Rack::Utils.escape(a.id) %>">Edit</a>
33
+ <a class="btn ghost" href="/studio/playground?agent=<%= Rack::Utils.escape(a.id) %>">Test</a>
34
+ </div>
35
+ </article>
36
+ <% end %>
37
+ </div>
38
+ <div class="empty" data-list-filter-target="empty" hidden>No agents match your filter.</div>
39
+ </div>
40
+ <% end %>
41
+
42
+ <details class="card newfile"<%== " open" if @agents.empty? %>>
43
+ <summary>+ new agent</summary>
44
+ <form method="post" action="/studio/agents">
45
+ <%== csrf_tag %>
46
+ <div class="grid-2">
47
+ <label>id <span class="muted">(unique slug, e.g. bia)</span>
48
+ <input type="text" name="id" placeholder="bia" required>
49
+ </label>
50
+ <label>model
51
+ <input type="text" name="model" placeholder="deepseek-v4-flash" required>
52
+ </label>
53
+ </div>
54
+ <label>provider <span class="muted">(optional)</span>
55
+ <input type="text" name="provider" placeholder="deepseek">
56
+ </label>
57
+ <label class="check">
58
+ <input type="checkbox" name="memory" value="1" checked>
59
+ cross-session memory (the BIA remembers across conversations)
60
+ </label>
61
+ <div class="actions"><button type="submit" class="primary" data-turbo-submits-with="Creating…">Create agent</button></div>
62
+ </form>
63
+ </details>
@@ -0,0 +1,41 @@
1
+ <header class="page-head">
2
+ <div>
3
+ <nav class="crumbs"><span>approvals</span></nav>
4
+ <h1>Approvals<span class="sub"><%= @approvals.size %> pending</span></h1>
5
+ </div>
6
+ <a class="btn" href="/studio/tasks">Tasks →</a>
7
+ </header>
8
+
9
+ <% if @approvals.empty? %>
10
+ <div class="empty">No pending approvals. Actions that need a human decision (approval-gated tools) land here.</div>
11
+ <% else %>
12
+ <% @approvals.each do |a| %>
13
+ <% pa = a[:pending]; task = a[:task] %>
14
+ <div class="card">
15
+ <div class="skill-name mono">
16
+ <%= pa.tool %>
17
+ <% if task %><span class="pill <%= status_class(task.status) %>"><%= task.status %></span><% end %>
18
+ </div>
19
+ <div class="skill-desc muted">
20
+ <a class="mono" href="<%= task_path(pa.task_id) %>">task <%= pa.task_id[0, 12] %></a>
21
+ · turn <%= pa.turn %> · requested <%= pa.requested_at %>
22
+ </div>
23
+ <pre><%= message_content(pa.args) %></pre>
24
+ <div class="actions">
25
+ <form method="post" action="/studio/approvals/<%= Rack::Utils.escape(pa.id) %>">
26
+ <%== csrf_tag %>
27
+ <input type="hidden" name="decision" value="approved">
28
+ <input type="hidden" name="back" value="/studio/approvals">
29
+ <button type="submit" class="primary">Approve</button>
30
+ </form>
31
+ <form method="post" action="/studio/approvals/<%= Rack::Utils.escape(pa.id) %>"
32
+ data-turbo-confirm="Reject this action? The turn resumes as if the tool was denied.">
33
+ <%== csrf_tag %>
34
+ <input type="hidden" name="decision" value="rejected">
35
+ <input type="hidden" name="back" value="/studio/approvals">
36
+ <button type="submit" class="danger">Reject</button>
37
+ </form>
38
+ </div>
39
+ </div>
40
+ <% end %>
41
+ <% end %>
@@ -0,0 +1,34 @@
1
+ <header class="page-head">
2
+ <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>
5
+ </div>
6
+ <a class="btn primary" href="/studio/playground">New conversation →</a>
7
+ </header>
8
+
9
+ <% if @sessions.empty? %>
10
+ <div class="empty">No conversations yet. Start one in the <a href="/studio/playground">playground</a>.</div>
11
+ <% else %>
12
+ <div data-controller="list-filter">
13
+ <div class="filter-bar">
14
+ <input type="search" class="filter-input" placeholder="Filter conversations by id or preview…" aria-label="Filter conversations"
15
+ data-list-filter-target="query" data-action="input->list-filter#filter keydown->list-filter#clear">
16
+ <span class="filter-count mono muted" data-list-filter-target="count"></span>
17
+ </div>
18
+ <div class="card">
19
+ <ul class="history">
20
+ <% @sessions.each do |s| %>
21
+ <% preview = session_preview(s) %>
22
+ <li data-list-filter-target="item" data-filter-text="<%= s.id %> <%= preview %>">
23
+ <a href="/studio/sessions/<%= Rack::Utils.escape(s.id) %>">
24
+ <span class="mono"><%= s.id[0, 12] %></span>
25
+ </a>
26
+ <span class="muted"><%= Array(s.messages).size %> msg · <%= s.updated_at %></span>
27
+ <% if preview %><span class="preview muted"><%= preview[0, 90] %></span><% end %>
28
+ </li>
29
+ <% end %>
30
+ </ul>
31
+ </div>
32
+ <div class="empty" data-list-filter-target="empty" hidden>No conversations match your filter.</div>
33
+ </div>
34
+ <% end %>
@@ -0,0 +1,83 @@
1
+ <header class="page-head">
2
+ <div>
3
+ <nav class="crumbs"><span>evals</span></nav>
4
+ <h1>Evals<span class="sub"><%= @cases.size %> case(s)</span></h1>
5
+ </div>
6
+ <a class="btn" href="/studio/settings?s=evals">Judges →</a>
7
+ </header>
8
+
9
+ <% if @invalid.any? %>
10
+ <div class="empty">
11
+ <strong><%= @invalid.size %> case(s) no longer validate</strong> and are SKIPPED by a run:
12
+ <span class="mono"><%= @invalid.join(", ") %></span>. Fix the YAML or re-import the corpus
13
+ (<code>insika evals:import</code>).
14
+ </div>
15
+ <% end %>
16
+
17
+ <div class="drill">
18
+ <aside class="drill-pane drill-master">
19
+ <div class="drill-pane-head"><div class="title"><strong>Cases</strong></div></div>
20
+ <nav class="drill-pane-body drill-list" aria-label="eval cases">
21
+ <% if @cases.empty? %>
22
+ <div class="empty">No case yet. Seed the corpus with <code>insika evals:import</code>, or write one on the right.</div>
23
+ <% end %>
24
+ <% @by_agent.each do |agent, cases| %>
25
+ <div class="nav-label"><%= agent %></div>
26
+ <% cases.each do |g| %>
27
+ <a class="drill-item<%== ' active' if @case && @case.id == g.id %>" href="/studio/evals?id=<%= Rack::Utils.escape(g.id) %>">
28
+ <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 %></div>
29
+ <div class="drill-item-sub"><%= g.user_turns.first %></div>
30
+ </a>
31
+ <% end %>
32
+ <% end %>
33
+ </nav>
34
+ </aside>
35
+
36
+ <section class="drill-pane drill-detail">
37
+ <div class="drill-pane-head">
38
+ <div class="title"><strong><%= @case ? @case.id : "New case" %></strong></div>
39
+ <div class="actions">
40
+ <% if @case %>
41
+ <form method="post" action="/studio/evals/<%= Rack::Utils.escape(@case.id) %>/delete"
42
+ data-turbo-confirm="Remove case &quot;<%= @case.id %>&quot;? The corpus on disk is the way back.">
43
+ <%== csrf_tag %>
44
+ <button type="submit" class="ghost danger" data-turbo-submits-with="Removing…">Remove</button>
45
+ </form>
46
+ <% end %>
47
+ <button type="submit" form="set-golden" class="btn primary" data-turbo-submits-with="Saving…">Save</button>
48
+ </div>
49
+ </div>
50
+ <div class="drill-pane-body">
51
+ <p class="muted">
52
+ A case is data, in the same YAML the corpus files hold — so this is what a pull
53
+ request reviews. <code>expect.rubric</code> is plain language and is the part a
54
+ judge scores; <code>tools_called</code> and <code>must_not</code> are checked
55
+ deterministically, with no model involved. Saved through
56
+ <code>write_golden</code> and validated on the way in.
57
+ </p>
58
+ <form id="set-golden" method="post" action="/studio/evals">
59
+ <%== csrf_tag %>
60
+ <input type="hidden" name="id" value="<%= @case&.id %>">
61
+ <label>case (YAML)
62
+ <textarea name="yaml" rows="22" spellcheck="false" class="mono"><%= @case_yaml || <<~YAML %>
63
+ id: my-agent-coupon
64
+ agent: my-agent
65
+ turns:
66
+ - user: "tem cupom de desconto?"
67
+ expect:
68
+ tools_called:
69
+ - search_voucher
70
+ must_not:
71
+ - pii_leak
72
+ - tool_error
73
+ rubric: |
74
+ Consults the active coupon with the tool and says what it is — does NOT
75
+ invent a code or a discount. If there is none, says so kindly.
76
+ min_score: 0.7
77
+ YAML
78
+ </textarea>
79
+ </label>
80
+ </form>
81
+ </div>
82
+ </section>
83
+ </div>
@@ -0,0 +1,72 @@
1
+ <%# Overview home (T3): stat cards + activity chart + recent conversations.
2
+ All from data the Studio already reads; charts are inline SVG (geometry via
3
+ attributes, fill via stylesheet — CSP-safe, no inline style). %>
4
+ <header class="page-head">
5
+ <div>
6
+ <h1>Overview<span class="sub">at a glance</span></h1>
7
+ </div>
8
+ <div class="head-actions">
9
+ <span class="pill <%= @active_now.positive? ? "ok online" : "info" %> plain"><%= @active_now %> active now</span>
10
+ <a class="btn primary" href="/studio/playground">Open playground →</a>
11
+ </div>
12
+ </header>
13
+
14
+ <div class="stat-cards">
15
+ <% [["conversations", "Conversations"], ["messages", "Messages"], ["agents", "Agents"],
16
+ ["skills", "Skills"], ["tools", "Tools"], ["providers", "LLM providers"]].each do |key, label| %>
17
+ <div class="stat-card">
18
+ <div class="stat-num"><%= @counts[key] %></div>
19
+ <div class="stat-label"><%= label %></div>
20
+ </div>
21
+ <% end %>
22
+ </div>
23
+
24
+ <div class="home-grid">
25
+ <section class="card home-activity">
26
+ <div class="section-head">
27
+ <h2>Activity</h2>
28
+ <span class="muted">conversations touched · last 14 days</span>
29
+ </div>
30
+ <% max = [@activity.map(&:last).max || 0, 1].max %>
31
+ <svg class="barchart" viewBox="0 0 700 176" preserveAspectRatio="none" role="img" aria-label="conversations per day, last 14 days">
32
+ <% n = @activity.size; slot = 700.0 / n; bw = slot * 0.58 %>
33
+ <% @activity.each_with_index do |(d, c), i| %>
34
+ <% h = (c.to_f / max) * 150.0; x = (i * slot) + ((slot - bw) / 2.0); y = 160.0 - h %>
35
+ <rect class="bar<%== ' bar-empty' if c.zero? %>" x="<%= format('%.1f', x) %>" y="<%= format('%.1f', c.zero? ? 157.0 : y) %>"
36
+ width="<%= format('%.1f', bw) %>" height="<%= format('%.1f', c.zero? ? 3.0 : [h, 3.0].max) %>" rx="2">
37
+ <title><%= d.strftime('%b %-d') %> · <%= c %> conversation<%= 's' unless c == 1 %></title>
38
+ </rect>
39
+ <% end %>
40
+ <line class="barchart-axis-line" x1="0" y1="160" x2="700" y2="160" />
41
+ </svg>
42
+ <div class="barchart-axis">
43
+ <span><%= @activity.first&.first&.strftime('%b %-d') %></span>
44
+ <span class="peak">peak <%= max %>/day</span>
45
+ <span><%= @activity.last&.first&.strftime('%b %-d') %></span>
46
+ </div>
47
+ </section>
48
+
49
+ <section class="card home-recent">
50
+ <div class="section-head">
51
+ <h2>Recent conversations</h2>
52
+ <a class="muted" href="/studio/chats">all →</a>
53
+ </div>
54
+ <% if @recent.empty? %>
55
+ <div class="empty">No conversations yet. Start one in the <a href="/studio/playground">playground</a>.</div>
56
+ <% else %>
57
+ <ul class="convo-rail">
58
+ <% @recent.each do |s| %>
59
+ <li>
60
+ <a href="/studio/sessions/<%= Rack::Utils.escape(s.id) %>">
61
+ <span class="avatar sm"><%= s.id[0, 2].upcase %></span>
62
+ <span class="convo-main">
63
+ <span class="convo-id mono"><%= s.id[0, 20] %></span>
64
+ <span class="convo-sub"><%= Array(s.messages).size %> msg · <%= time_ago(s.updated_at) %></span>
65
+ </span>
66
+ </a>
67
+ </li>
68
+ <% end %>
69
+ </ul>
70
+ <% end %>
71
+ </section>
72
+ </div>