insika 0.9.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 (257) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +237 -2
  3. data/README.md +5 -3
  4. data/bin/insika +3 -0
  5. data/bin/insika-router +0 -1
  6. data/docs/AGENTS.md +90 -7
  7. data/docs/ARCHITECTURE.md +12 -7
  8. data/docs/ARTIFACTS.md +33 -3
  9. data/docs/BENCHMARK.md +50 -7
  10. data/docs/CONTEXT.md +63 -0
  11. data/docs/CROSS-HARNESS-BENCH.md +157 -0
  12. data/docs/DEPLOY.md +57 -11
  13. data/docs/EMBEDDING.md +10 -0
  14. data/docs/EVALS.md +33 -4
  15. data/docs/FACTS.md +8 -6
  16. data/docs/HARVEST.md +8 -3
  17. data/docs/KNOWLEDGE.md +7 -0
  18. data/docs/MEDIA.md +6 -2
  19. data/docs/OBSERVABILITY.md +143 -22
  20. data/docs/PERFORMANCE.md +110 -0
  21. data/docs/PLUGINS.md +4 -4
  22. data/docs/POLICY.md +22 -13
  23. data/docs/RELEASING.md +5 -0
  24. data/docs/ROUTER.md +7 -0
  25. data/docs/RUBYLLM_2_MIGRATION.md +307 -0
  26. data/docs/SECURITY.md +7 -0
  27. data/docs/SHARED-CONVERSATIONS.md +40 -0
  28. data/docs/SKILLS.md +5 -3
  29. data/docs/STUDIO.md +122 -0
  30. data/docs/TOOLS.md +111 -9
  31. data/docs/operate.md +3 -0
  32. data/lib/insika/agent_defaults.rb +140 -0
  33. data/lib/insika/agent_file_store.rb +12 -0
  34. data/lib/insika/agent_profile.rb +78 -8
  35. data/lib/insika/artifact_signing.rb +11 -0
  36. data/lib/insika/chat_builder.rb +79 -25
  37. data/lib/insika/checkpoint.rb +6 -1
  38. data/lib/insika/checkpoint_store.rb +59 -7
  39. data/lib/insika/commands/agent_payload.rb +3 -3
  40. data/lib/insika/commands/delete_kit.rb +26 -0
  41. data/lib/insika/commands/gate_harvest.rb +3 -0
  42. data/lib/insika/commands/review_guardrail_flag.rb +38 -0
  43. data/lib/insika/commands/run_harvest.rb +17 -1
  44. data/lib/insika/commands/seed_session.rb +11 -1
  45. data/lib/insika/commands/send_message.rb +9 -3
  46. data/lib/insika/commands/update_settings.rb +23 -1
  47. data/lib/insika/commands/write_kit.rb +27 -0
  48. data/lib/insika/compaction.rb +33 -7
  49. data/lib/insika/config_store.rb +87 -7
  50. data/lib/insika/context/priority.rb +3 -0
  51. data/lib/insika/context/provider.rb +2 -2
  52. data/lib/insika/context/providers/knowledge.rb +25 -2
  53. data/lib/insika/context/providers/memory.rb +53 -4
  54. data/lib/insika/context/providers/pending_confirmation.rb +52 -0
  55. data/lib/insika/context/providers/prompt.rb +17 -4
  56. data/lib/insika/context/providers/session.rb +39 -3
  57. data/lib/insika/context/reranker.rb +75 -0
  58. data/lib/insika/distill.rb +22 -13
  59. data/lib/insika/doctor.rb +34 -4
  60. data/lib/insika/dsl/runtime.rb +36 -4
  61. data/lib/insika/dsl/server_boot.rb +4 -12
  62. data/lib/insika/dsl.rb +13 -5
  63. data/lib/insika/edge_limiter.rb +23 -15
  64. data/lib/insika/egress_guard.rb +11 -3
  65. data/lib/insika/env_schema.rb +33 -73
  66. data/lib/insika/evals/assertions.rb +171 -16
  67. data/lib/insika/evals/golden.rb +141 -4
  68. data/lib/insika/evals/judge.rb +19 -10
  69. data/lib/insika/evals/moderator_compare.rb +47 -0
  70. data/lib/insika/evals/report.rb +61 -9
  71. data/lib/insika/evals/runner.rb +44 -4
  72. data/lib/insika/evals/simulator.rb +10 -1
  73. data/lib/insika/evals/transport.rb +200 -0
  74. data/lib/insika/event_stream.rb +63 -7
  75. data/lib/insika/evidence.rb +29 -8
  76. data/lib/insika/executor.rb +668 -141
  77. data/lib/insika/harvest.rb +26 -14
  78. data/lib/insika/harvest_engine.rb +1 -0
  79. data/lib/insika/harvest_store.rb +12 -0
  80. data/lib/insika/http_client.rb +87 -4
  81. data/lib/insika/kits.rb +54 -0
  82. data/lib/insika/knowledge.rb +34 -20
  83. data/lib/insika/llm_configurator.rb +46 -4
  84. data/lib/insika/llm_provider_store.rb +1 -3
  85. data/lib/insika/llm_trace_store.rb +54 -0
  86. data/lib/insika/mcp_client.rb +27 -63
  87. data/lib/insika/mcp_live_tool.rb +55 -9
  88. data/lib/insika/mcp_store.rb +32 -1
  89. data/lib/insika/mcp_tool_registry.rb +69 -26
  90. data/lib/insika/media.rb +38 -25
  91. data/lib/insika/memory_store.rb +4 -3
  92. data/lib/insika/memory_trim.rb +60 -0
  93. data/lib/insika/middleware.rb +12 -3
  94. data/lib/insika/model_metrics_store.rb +285 -0
  95. data/lib/insika/model_registry.rb +110 -0
  96. data/lib/insika/model_resolver.rb +1 -1
  97. data/lib/insika/model_selection.rb +14 -21
  98. data/lib/insika/model_visible.rb +17 -6
  99. data/lib/insika/onboarding.rb +1 -0
  100. data/lib/insika/outcome_store.rb +1 -1
  101. data/lib/insika/overlay_tool_registry.rb +25 -2
  102. data/lib/insika/pack_importer.rb +54 -9
  103. data/lib/insika/packaging.rb +14 -5
  104. data/lib/insika/pending_action_store.rb +45 -18
  105. data/lib/insika/plugin/loader.rb +4 -2
  106. data/lib/insika/profile_source.rb +4 -0
  107. data/lib/insika/provider_error_classifier.rb +18 -3
  108. data/lib/insika/queue_policy.rb +7 -2
  109. data/lib/insika/refinement/proposer.rb +42 -17
  110. data/lib/insika/reliability.rb +12 -3
  111. data/lib/insika/router/app.rb +14 -5
  112. data/lib/insika/router/session_key.rb +35 -5
  113. data/lib/insika/safety/config.rb +20 -3
  114. data/lib/insika/safety/corpus.rb +10 -3
  115. data/lib/insika/safety/factory.rb +39 -1
  116. data/lib/insika/safety/input_guardrail.rb +21 -1
  117. data/lib/insika/safety/judge_moderator.rb +70 -0
  118. data/lib/insika/safety/output_validator.rb +29 -5
  119. data/lib/insika/safety/prompt_echo.rb +89 -0
  120. data/lib/insika/schema_guard.rb +53 -76
  121. data/lib/insika/server/app.rb +17 -8
  122. data/lib/insika/server/responses.rb +19 -1
  123. data/lib/insika/session_actor.rb +58 -28
  124. data/lib/insika/session_store.rb +159 -10
  125. data/lib/insika/settings_store.rb +47 -1
  126. data/lib/insika/shared_conversations.rb +305 -0
  127. data/lib/insika/skill_catalog.rb +28 -2
  128. data/lib/insika/skill_store.rb +6 -0
  129. data/lib/insika/soak/runner.rb +1 -1
  130. data/lib/insika/steer_injector.rb +7 -48
  131. data/lib/insika/store.rb +29 -1
  132. data/lib/insika/stores/json_value.rb +37 -0
  133. data/lib/insika/stores/memory.rb +22 -35
  134. data/lib/insika/stores/postgres.rb +295 -0
  135. data/lib/insika/stores/sqlite.rb +257 -43
  136. data/lib/insika/stores/turn_counter.rb +25 -0
  137. data/lib/insika/studio/app.rb +1453 -161
  138. data/lib/insika/studio/assets/dist/application.css +1 -1
  139. data/lib/insika/studio/assets/dist/application.js +25 -25
  140. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  141. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  142. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  143. data/lib/insika/studio/assets/dist/fonts/ibm-plex-sans-latin-wght-normal.woff2 +0 -0
  144. data/lib/insika/studio/forms.rb +171 -9
  145. data/lib/insika/studio/nav_icons.rb +3 -0
  146. data/lib/insika/studio/views/_agent_config_retrieval.erb +73 -0
  147. data/lib/insika/studio/views/_agent_picker.erb +15 -0
  148. data/lib/insika/studio/views/_agent_tab_config.erb +78 -242
  149. data/lib/insika/studio/views/_agent_tab_history.erb +56 -1
  150. data/lib/insika/studio/views/_agent_tab_performance.erb +34 -0
  151. data/lib/insika/studio/views/_agent_tab_prompts.erb +7 -9
  152. data/lib/insika/studio/views/_agent_tab_skills.erb +16 -7
  153. data/lib/insika/studio/views/_agent_tab_tools.erb +90 -0
  154. data/lib/insika/studio/views/_agents_master.erb +8 -12
  155. data/lib/insika/studio/views/_cfg_alerts.erb +4 -0
  156. data/lib/insika/studio/views/_cfg_budget.erb +20 -0
  157. data/lib/insika/studio/views/_cfg_distill.erb +26 -0
  158. data/lib/insika/studio/views/_cfg_followup.erb +30 -0
  159. data/lib/insika/studio/views/_cfg_grounding.erb +21 -0
  160. data/lib/insika/studio/views/_cfg_guardrails.erb +47 -0
  161. data/lib/insika/studio/views/_cfg_harvest.erb +35 -0
  162. data/lib/insika/studio/views/_cfg_reliability.erb +32 -0
  163. data/lib/insika/studio/views/_chats_master.erb +66 -0
  164. data/lib/insika/studio/views/_customers_master.erb +37 -0
  165. data/lib/insika/studio/views/_empty.erb +15 -0
  166. data/lib/insika/studio/views/_facts_master.erb +42 -0
  167. data/lib/insika/studio/views/_harvest_candidate_detail.erb +14 -0
  168. data/lib/insika/studio/views/_identity.erb +16 -0
  169. data/lib/insika/studio/views/_kpi.erb +17 -0
  170. data/lib/insika/studio/views/_message.erb +40 -2
  171. data/lib/insika/studio/views/_model_chart.erb +82 -0
  172. data/lib/insika/studio/views/_refinement_master.erb +38 -0
  173. data/lib/insika/studio/views/{tool_edit.erb → _tool_edit.erb} +32 -9
  174. data/lib/insika/studio/views/_tool_users.erb +17 -0
  175. data/lib/insika/studio/views/_why.erb +53 -0
  176. data/lib/insika/studio/views/agent_detail.erb +17 -10
  177. data/lib/insika/studio/views/agents.erb +28 -16
  178. data/lib/insika/studio/views/approvals.erb +16 -11
  179. data/lib/insika/studio/views/artifact.erb +20 -7
  180. data/lib/insika/studio/views/artifacts.erb +19 -14
  181. data/lib/insika/studio/views/chats.erb +22 -29
  182. data/lib/insika/studio/views/customer.erb +88 -43
  183. data/lib/insika/studio/views/customers.erb +16 -26
  184. data/lib/insika/studio/views/evals.erb +49 -31
  185. data/lib/insika/studio/views/fact_detail.erb +114 -0
  186. data/lib/insika/studio/views/facts.erb +15 -116
  187. data/lib/insika/studio/views/followups.erb +63 -52
  188. data/lib/insika/studio/views/funnel.erb +104 -43
  189. data/lib/insika/studio/views/harvest.erb +85 -59
  190. data/lib/insika/studio/views/home.erb +138 -23
  191. data/lib/insika/studio/views/inbox.erb +36 -0
  192. data/lib/insika/studio/views/kits.erb +176 -0
  193. data/lib/insika/studio/views/knowledge.erb +92 -80
  194. data/lib/insika/studio/views/layout.erb +42 -11
  195. data/lib/insika/studio/views/live.erb +45 -0
  196. data/lib/insika/studio/views/mcp.erb +43 -13
  197. data/lib/insika/studio/views/models.erb +187 -0
  198. data/lib/insika/studio/views/parity.erb +38 -32
  199. data/lib/insika/studio/views/playground.erb +13 -9
  200. data/lib/insika/studio/views/refinement.erb +15 -224
  201. data/lib/insika/studio/views/refinement_detail.erb +243 -0
  202. data/lib/insika/studio/views/session.erb +152 -117
  203. data/lib/insika/studio/views/settings.erb +86 -22
  204. data/lib/insika/studio/views/skills.erb +53 -19
  205. data/lib/insika/studio/views/system_files.erb +7 -7
  206. data/lib/insika/studio/views/task.erb +45 -15
  207. data/lib/insika/studio/views/tasks.erb +35 -18
  208. data/lib/insika/studio/views/tools.erb +62 -73
  209. data/lib/insika/task_actor.rb +4 -1
  210. data/lib/insika/task_store.rb +67 -1
  211. data/lib/insika/telemetry/pricing.rb +16 -14
  212. data/lib/insika/telemetry/recorder.rb +103 -9
  213. data/lib/insika/telemetry/ruby_llm_instrumenter.rb +110 -0
  214. data/lib/insika/telemetry.rb +12 -2
  215. data/lib/insika/templates/daily-digest/README.md +13 -0
  216. data/lib/insika/templates/daily-digest/agent.rb +22 -11
  217. data/lib/insika/testing/store_contract.rb +65 -0
  218. data/lib/insika/text_tool_call.rb +66 -0
  219. data/lib/insika/tick.rb +7 -1
  220. data/lib/insika/token_store.rb +5 -0
  221. data/lib/insika/tool_batch.rb +1 -1
  222. data/lib/insika/tool_catalog.rb +12 -2
  223. data/lib/insika/tool_definition.rb +10 -4
  224. data/lib/insika/tool_envelope.rb +165 -28
  225. data/lib/insika/tool_manifest.rb +1 -0
  226. data/lib/insika/tool_store.rb +3 -0
  227. data/lib/insika/tool_trace_store.rb +15 -8
  228. data/lib/insika/tool_usage_report.rb +2 -2
  229. data/lib/insika/tools/a2a_remote.rb +1 -1
  230. data/lib/insika/tools/agent_enum.rb +1 -1
  231. data/lib/insika/tools/confirm_pending.rb +112 -0
  232. data/lib/insika/tools/data_defined_tool.rb +23 -15
  233. data/lib/insika/tools/generate_image.rb +2 -2
  234. data/lib/insika/tools/load_knowledge.rb +7 -3
  235. data/lib/insika/tools/load_skill.rb +3 -3
  236. data/lib/insika/tools/present.rb +1 -8
  237. data/lib/insika/tools/remember.rb +7 -2
  238. data/lib/insika/tools/run_persona_eval.rb +3 -5
  239. data/lib/insika/tools/save_artifact.rb +16 -6
  240. data/lib/insika/tools/schedule_followup.rb +3 -3
  241. data/lib/insika/tools/stuck_signal.rb +3 -3
  242. data/lib/insika/tools/subagent.rb +5 -5
  243. data/lib/insika/tools/subagents.rb +3 -3
  244. data/lib/insika/tools/tool_search.rb +7 -5
  245. data/lib/insika/tools/tts.rb +2 -2
  246. data/lib/insika/tools/update_briefing.rb +4 -4
  247. data/lib/insika/turn_output.rb +2 -2
  248. data/lib/insika/turn_state.rb +18 -0
  249. data/lib/insika/turn_timing.rb +51 -14
  250. data/lib/insika/version.rb +1 -1
  251. data/lib/insika/wiring/graph.rb +81 -16
  252. data/lib/insika/workflow.rb +25 -64
  253. data/lib/insika.rb +19 -3
  254. metadata +69 -13
  255. data/lib/insika/commands/import_mcp_tools.rb +0 -48
  256. data/lib/insika/mcp_http_client.rb +0 -67
  257. data/lib/insika/mcp_tool_ingestor.rb +0 -165
@@ -2,7 +2,8 @@
2
2
  <!-- ── Config / model ────────────────────────────────────────────────────── -->
3
3
  <section class="section" id="config" data-tabs-target="panel">
4
4
  <% config_groups = [
5
- ["model", "Model & limits", "model, provider, timeouts, ceilings, generation, fence"],
5
+ ["model", "Model & limits", "model, generation and runtime limits"],
6
+ ["retrieval", "Memory & knowledge", "remember, learn, retrieve and rerank"],
6
7
  ["guardrails", "Guardrails", "input/output checks, strictness, safe replies"],
7
8
  ["grounding", "Grounding", "evidence behind product claims"],
8
9
  ["funnel", "Outcome funnel", "stage vocabulary"],
@@ -17,10 +18,13 @@
17
18
  ]
18
19
  active = config_groups.find { |k, _| k == @config_group } || config_groups.first
19
20
  @config_group = active[0] %>
20
- <div class="section-head"><h2>Configuration</h2><p class="muted">every profile field, editable hot — pick a group to the left — saved via <code>update_agent</code>.</p></div>
21
+ <%# The group nav is its own master column (like Prompts' file list) — not a
22
+ "Configuration" card wrapping a duplicate "Configuration" card. Its own
23
+ drill-pane-head carries the title; the detail pane's head carries the
24
+ active group's name + description, same as every other drill-detail. %>
21
25
  <div class="drill config-drill">
22
26
  <aside class="drill-pane drill-master">
23
- <div class="drill-pane-head"><div class="title"><strong>Configuration</strong></div></div>
27
+ <div class="drill-pane-head"><div class="title"><strong>Configuration <span class="count"><%= config_groups.size %></span></strong></div></div>
24
28
  <nav class="drill-pane-body drill-list" aria-label="config groups">
25
29
  <% config_groups.each do |key, label, sub| %>
26
30
  <a class="drill-item<%== ' active' if @config_group == key %>"
@@ -34,21 +38,41 @@
34
38
  </aside>
35
39
  <section class="drill-pane drill-detail">
36
40
  <div class="drill-pane-head">
37
- <div class="title"><strong><%= active[1] %></strong></div>
41
+ <div class="title"><strong><%= active[1] %></strong><span class="count"><%= active[2] %></span></div>
38
42
  <button type="submit" form="config-form" class="btn primary" data-turbo-submits-with="Saving…">Save config</button>
39
43
  </div>
40
44
  <div class="drill-pane-body">
41
- <form id="config-form" method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/config">
45
+ <form id="config-form" method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/config" data-controller="dirty-guard">
42
46
  <%== csrf_tag %>
43
47
  <input type="hidden" name="cfg" value="<%= @config_group %>">
48
+ <p class="muted">Save config applies changes to <strong><%= @agent.id %></strong>. Changes take effect on future turns.</p>
44
49
  <div class="config-group" data-config-group="model"<%== " hidden" unless @config_group == "model" %>>
45
- <div class="grid-2">
46
- <label>model <span class="muted">(blank = platform <code>default_model</code>)</span>
47
- <input type="text" name="model" value="<%= @agent.model %>" placeholder="inherit platform default">
48
- </label>
49
- <label>provider
50
- <input type="text" name="provider" value="<%= @agent.provider %>" placeholder="deepseek">
50
+ <fieldset class="subgroup">
51
+ <legend>Model</legend>
52
+ <label>model <span class="muted">(blank = platform <code>default_model</code>; the list is Settings › LLM providers)</span>
53
+ <%== model_select("model_ref", model_ref_of(@agent.model, @agent.provider), "inherit platform default") %>
51
54
  </label>
55
+ </fieldset>
56
+
57
+ <fieldset class="subgroup">
58
+ <legend>Response generation <span class="muted">— blank = provider default</span></legend>
59
+ <div class="grid-3">
60
+ <label>temperature
61
+ <input type="text" name="temperature" value="<%= agent_param(@params, :temperature) %>" inputmode="decimal" placeholder="0.7">
62
+ </label>
63
+ <label>max_tokens
64
+ <input type="text" name="max_tokens" value="<%= agent_param(@params, :max_tokens) %>" inputmode="numeric" placeholder="4096">
65
+ </label>
66
+ <label>reasoning <span class="muted">(off/on/effort — blank = inherit model/platform)</span>
67
+ <%== thinking_select("thinking", agent_param(@params, :thinking), "inherit") %>
68
+ </label>
69
+ </div>
70
+ </fieldset>
71
+
72
+ <details class="newfile">
73
+ <summary>Runtime limits &amp; allowed models</summary>
74
+ <p class="muted">Control turn duration, tool concurrency and usage. Blank limits inherit platform defaults where indicated.</p>
75
+ <div class="grid-2">
52
76
  <label>turn_timeout (s)
53
77
  <input type="text" name="turn_timeout" value="<%= @agent.limits[:turn_timeout] %>" inputmode="numeric">
54
78
  </label>
@@ -64,6 +88,16 @@
64
88
  <label>agent_token_ceiling <span class="muted">(tokens/window; blank = platform, 0 = off)</span>
65
89
  <input type="text" name="agent_token_ceiling" value="<%= @agent.limits[:agent_token_ceiling] %>" inputmode="numeric" placeholder="inherit platform edge">
66
90
  </label>
91
+ </div>
92
+ <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>
93
+ <textarea name="model_policy_allow" rows="3" placeholder="deepseek/*&#10;openai/gpt-4o-mini"><%= @model_policy_allow %></textarea>
94
+ </label>
95
+ </details>
96
+
97
+ <details class="newfile">
98
+ <summary>Messages received while busy</summary>
99
+ <p class="muted">Choose whether new messages wait, combine, steer or interrupt a running turn.</p>
100
+ <div class="grid-2">
67
101
  <label>queue_mode <span class="muted">(burst policy: followup | collect | steer | interrupt; blank = platform/default followup)</span>
68
102
  <select name="queue_mode">
69
103
  <option value=""<%== " selected" if @agent.limits[:queue_mode].nil? %>>— inherit</option>
@@ -75,99 +109,35 @@
75
109
  <label>steer_max_messages <span class="muted">(messages ONE running turn may absorb; blank = default 5, 0 = never steer)</span>
76
110
  <input type="text" name="steer_max_messages" value="<%= @agent.limits[:steer_max_messages] %>" inputmode="numeric" placeholder="5">
77
111
  </label>
78
- </div>
79
- <label class="check">
80
- <input type="checkbox" name="memory" value="1"<%== " checked" if @agent.memory %>>
81
- cross-session memory (the agent remembers across conversations)
82
- </label>
83
-
84
- <fieldset class="subgroup">
85
- <legend>generation params <span class="muted">— blank = provider default</span></legend>
86
- <div class="grid-3">
87
- <label>temperature
88
- <input type="text" name="temperature" value="<%= agent_param(@params, :temperature) %>" inputmode="decimal" placeholder="0.7">
89
- </label>
90
- <label>max_tokens
91
- <input type="text" name="max_tokens" value="<%= agent_param(@params, :max_tokens) %>" inputmode="numeric" placeholder="4096">
92
- </label>
93
- <label>reasoning <span class="muted">(off/on/effort — blank = inherit model/platform)</span>
94
- <%== thinking_select("thinking", agent_param(@params, :thinking), "inherit") %>
95
- </label>
112
+ <label>debounce_ms <span class="muted">(extra quiet window at the door; blank/0 = none — collect/steer still merge into a turn that is queued behind another)</span>
113
+ <input type="text" name="debounce_ms" value="<%= @agent.limits[:debounce_ms] %>" inputmode="numeric" placeholder="0">
114
+ </label>
115
+ <label>debounce_max_ms <span class="muted">(ceiling on the TOTAL wait; blank = default 10000)</span>
116
+ <input type="text" name="debounce_max_ms" value="<%= @agent.limits[:debounce_max_ms] %>" inputmode="numeric" placeholder="10000">
117
+ </label>
118
+ <label>steer_join <span class="muted">(template framing a mid-run message; must carry <%= Insika::QueuePolicy::JOIN_PLACEHOLDER %>; blank = the raw text)</span>
119
+ <input type="text" name="steer_join" value="<%= @agent.limits[:steer_join] %>" placeholder="the customer just added: <%= Insika::QueuePolicy::JOIN_PLACEHOLDER %>">
120
+ </label>
96
121
  </div>
97
- </fieldset>
122
+ </details>
98
123
 
99
- <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>
100
- <textarea name="model_policy_allow" rows="3" placeholder="deepseek/*&#10;openai/gpt-4o-mini"><%= @model_policy_allow %></textarea>
101
- </label>
124
+ </div>
125
+
126
+ <div class="config-group" data-config-group="retrieval"<%== " hidden" unless @config_group == "retrieval" %>>
127
+ <%== render("_agent_config_retrieval") %>
102
128
  </div>
103
129
 
104
130
  <div class="config-group" data-config-group="guardrails"<%== " hidden" unless @config_group == "guardrails" %>>
105
- <fieldset class="subgroup">
106
- <legend>guardrails <span class="muted">— content safety</span></legend>
107
- <label class="check">
108
- <input type="checkbox" name="guardrail_input" value="1"<%== " checked" if guardrail_field(@guardrails, :input, true) %>>
109
- input guardrail <span class="muted">— block injection / abuse / inappropriate before the LLM</span>
110
- </label>
111
- <label class="check">
112
- <input type="checkbox" name="guardrail_output" value="1"<%== " checked" if guardrail_field(@guardrails, :output, true) %>>
113
- output guardrail <span class="muted">— redact PII/secret in the stream + flag policy slips</span>
114
- </label>
115
- <div class="grid-2">
116
- <label>strictness
117
- <% strictness = guardrail_field(@guardrails, :strictness, "medium").to_s %>
118
- <select name="guardrail_strictness">
119
- <% %w[low medium high].each do |lvl| %>
120
- <option value="<%= lvl %>"<%== " selected" if lvl == strictness %>><%= lvl %></option>
121
- <% end %>
122
- </select>
123
- </label>
124
- <label>LLM moderator <span class="muted">(model ref, or <code>on</code> for the platform utility_model; blank = deterministic only)</span>
125
- <input type="text" name="guardrail_moderator" value="<%= guardrail_field(@guardrails, :moderator, "") %>" placeholder="deepseek/deepseek-v4-flash">
126
- </label>
127
- </div>
128
- <details class="newfile">
129
- <summary>safe replies — override per category <span class="muted">(blank = engine default; this agent's voice/language)</span></summary>
130
- <% gr_resp = (@guardrails["responses"] || @guardrails[:responses] || {}) %>
131
- <label>default <span class="muted">(catch-all for any blocked category)</span>
132
- <textarea name="guardrail_response_default" rows="2" placeholder="I can't help with that particular request…"><%= guardrail_field(gr_resp, :default, "") %></textarea>
133
- </label>
134
- <div class="grid-2">
135
- <% %w[injection sexual abuse escalate].each do |cat| %>
136
- <label><%= cat %>
137
- <textarea name="guardrail_response_<%= cat %>" rows="2"><%= guardrail_field(gr_resp, cat, "") %></textarea>
138
- </label>
139
- <% end %>
140
- </div>
141
- </details>
142
- </fieldset>
131
+ <%== render("_cfg_guardrails") %>
143
132
  </div>
144
133
 
145
134
  <div class="config-group" data-config-group="grounding"<%== " hidden" unless @config_group == "grounding" %>>
146
- <fieldset class="subgroup">
147
- <legend>grounding <span class="muted">— product claims must point at evidence the session actually saw</span></legend>
148
- <% gr = @agent.grounding || {} %>
149
- <div class="grid-2">
150
- <label>mode <span class="muted">(blank = off)</span>
151
- <select name="grounding_mode">
152
- <option value="">off</option>
153
- <% %w[flag enforce].each do |m| %>
154
- <option value="<%= m %>"<%== ' selected' if gr["mode"] == m %>><%= m %></option>
155
- <% end %>
156
- </select>
157
- </label>
158
- <label>matcher SKU <span class="muted">(evidence id prefix, e.g. <code>sku:</code>)</span>
159
- <input type="text" name="grounding_matcher_sku" value="<%= gr.dig("matcher", "sku") %>" placeholder="sku:">
160
- </label>
161
- <label>name keys <span class="muted">(comma-separated metadata keys with product names)</span>
162
- <input type="text" name="grounding_name_keys" value="<%= agent_list(gr.dig("matcher", "name_keys")) %>" placeholder="product, name">
163
- </label>
164
- </div>
165
- </fieldset>
135
+ <%== render("_cfg_grounding") %>
166
136
  </div>
167
137
 
168
138
  <div class="config-group" data-config-group="funnel"<%== " hidden" unless @config_group == "funnel" %>>
169
139
  <fieldset class="subgroup">
170
- <legend>outcome funnel <span class="muted">— the stage vocabulary the engine folds WS7 outcomes into</span></legend>
140
+ <legend>outcome funnel <span class="muted">— the stages used to track recorded outcomes</span></legend>
171
141
  <% fun = @agent.funnel || {} %>
172
142
  <div class="grid-2">
173
143
  <label>stages <span class="muted">(one per line, in order; blank = no funnel)</span>
@@ -190,35 +160,7 @@
190
160
  </div>
191
161
 
192
162
  <div class="config-group" data-config-group="followups"<%== " hidden" unless @config_group == "followups" %>>
193
- <fieldset class="subgroup">
194
- <legend>follow-ups <span class="muted">— the seller who comes back (blank arm = off)</span></legend>
195
- <% fu = @agent.followup || {}; fup = fu["policy"] || {}; fqh = fup["quiet_hours"] || {} %>
196
- <div class="grid-2">
197
- <label>arm
198
- <input type="text" name="followup_arm" value="<%= fu["arm"] %>" placeholder="schedule">
199
- </label>
200
- <label>max frequency <span class="muted">(e.g. <code>2/24h</code>)</span>
201
- <input type="text" name="followup_max_frequency" value="<%= fup["max_frequency"] %>" placeholder="2/24h">
202
- </label>
203
- <label>quiet hours timezone <span class="muted">(blank = no quiet hours)</span>
204
- <input type="text" name="followup_tz" value="<%= fqh["timezone"] %>" placeholder="America/Sao_Paulo">
205
- </label>
206
- <div class="grid-2">
207
- <label>quiet start
208
- <input type="text" name="followup_quiet_start" value="<%= fqh["start"] %>" placeholder="21:30">
209
- </label>
210
- <label>quiet end
211
- <input type="text" name="followup_quiet_end" value="<%= fqh["end"] %>" placeholder="09:00">
212
- </label>
213
- </div>
214
- <label>cancel keywords <span class="muted">(comma-separated)</span>
215
- <input type="text" name="followup_cancel_keywords" value="<%= agent_list(fup["cancel_keywords"]) %>" placeholder="não quero mais contato">
216
- </label>
217
- <label>silence after N sends
218
- <input type="text" name="followup_silence_after_sends" value="<%= fup["silence_after_sends"] %>" placeholder="3" inputmode="numeric">
219
- </label>
220
- </div>
221
- </fieldset>
163
+ <%== render("_cfg_followup") %>
222
164
  </div>
223
165
 
224
166
  <div class="config-group" data-config-group="schedules"<%== " hidden" unless @config_group == "schedules" %>>
@@ -251,68 +193,11 @@
251
193
  </div>
252
194
 
253
195
  <div class="config-group" data-config-group="distill"<%== " hidden" unless @config_group == "distill" %>>
254
- <fieldset class="subgroup">
255
- <legend>distillation <span class="muted">— finished conversations → proposed customer facts (Facts page)</span></legend>
256
- <% dist = @agent.distill || {} %>
257
- <label class="check">
258
- <input type="checkbox" name="distill_enabled" value="1"<%== " checked" if Insika::Coercion.truthy?(dist["enabled"]) %>>
259
- distillation on — idle sessions are read back as fact proposals; a human always approves
260
- </label>
261
- <div class="grid-3">
262
- <label>idle hours <span class="muted">(blank = 6)</span>
263
- <input type="text" name="distill_idle_hours" value="<%= dist["idle_hours"] %>" placeholder="6" inputmode="numeric">
264
- </label>
265
- <label>min messages <span class="muted">(blank = 3)</span>
266
- <input type="text" name="distill_min_messages" value="<%= dist["min_messages"] %>" placeholder="3" inputmode="numeric">
267
- </label>
268
- <label>max proposals <span class="muted">(blank = 10)</span>
269
- <input type="text" name="distill_max_proposals" value="<%= dist["max_proposals"] %>" placeholder="10" inputmode="numeric">
270
- </label>
271
- </div>
272
- <label>model <span class="muted">(blank = the platform utility_model)</span>
273
- <input type="text" name="distill_model" value="<%= dist["model"] %>" placeholder="deepseek-v4-flash">
274
- </label>
275
- <label>prompt <span class="muted">— what counts as a fact for THIS store (blank = the engine default)</span>
276
- <textarea name="distill_prompt" rows="3" placeholder="what counts as a customer fact for this store"><%= dist["prompt"] %></textarea>
277
- </label>
278
- </fieldset>
196
+ <%== render("_cfg_distill") %>
279
197
  </div>
280
198
 
281
199
  <div class="config-group" data-config-group="harvest"<%== " hidden" unless @config_group == "harvest" %>>
282
- <fieldset class="subgroup">
283
- <legend>harvest <span class="muted">— finished traffic → proposed SKILLs, double-gated, human-promoted (Harvest page)</span></legend>
284
- <% hv = @agent.harvest || {}; hvm = hv["miner"] || {} %>
285
- <label class="check">
286
- <input type="checkbox" name="harvest_enabled" value="1"<%== " checked" if Insika::Coercion.truthy?(hv["enabled"]) %>>
287
- harvest on — idle conversations are mined for skills; a human promotes every one
288
- </label>
289
- <div class="grid-3">
290
- <label>idle hours <span class="muted">(blank = 24)</span>
291
- <input type="text" name="harvest_idle_hours" value="<%= hv["idle_hours"] %>" placeholder="24" inputmode="numeric">
292
- </label>
293
- <label>min messages <span class="muted">(blank = 3)</span>
294
- <input type="text" name="harvest_min_messages" value="<%= hv["min_messages"] %>" placeholder="3" inputmode="numeric">
295
- </label>
296
- <label>miner model <span class="muted">(blank = the platform utility_model)</span>
297
- <input type="text" name="harvest_miner_model" value="<%= hvm["model"] %>" placeholder="deepseek-v4-flash">
298
- </label>
299
- <label>miner window <span class="muted">(last N conversations)</span>
300
- <input type="text" name="harvest_miner_window" value="<%= hvm.dig("window", "last_sessions") %>" placeholder="200" inputmode="numeric">
301
- </label>
302
- <label>miner max proposals
303
- <input type="text" name="harvest_miner_max_proposals" value="<%= hvm["max_proposals"] %>" placeholder="10" inputmode="numeric">
304
- </label>
305
- <label>miner budget (tokens)
306
- <input type="text" name="harvest_miner_budget_tokens" value="<%= hvm.dig("budget", "tokens") %>" placeholder="20000" inputmode="numeric">
307
- </label>
308
- </div>
309
- <label>negative list <span class="muted">— JSON: <code>[{ "rule": "…", "pattern": "…" }]</code> (blank = the deployment seed)</span>
310
- <textarea name="harvest_negative_list" rows="3" spellcheck="false" class="mono" placeholder='[ { "rule": "no-competitor-prices", "pattern": "concorrente" } ]'><%= agent_json(hv["negative_list"]) %></textarea>
311
- </label>
312
- <label>prompt <span class="muted">— what a harvestable skill is for THIS store (blank = the engine default)</span>
313
- <textarea name="harvest_prompt" rows="3" placeholder="what makes a skill harvestable for this store"><%= hv["prompt"] %></textarea>
314
- </label>
315
- </fieldset>
200
+ <%== render("_cfg_harvest") %>
316
201
  </div>
317
202
 
318
203
  <div class="config-group" data-config-group="refinement"<%== " hidden" unless @config_group == "refinement" %>>
@@ -340,9 +225,9 @@
340
225
  <label>files <span class="muted">(allowlist, comma-separated)</span>
341
226
  <input type="text" name="refinement_files" value="<%= agent_list(rf["files"]) %>" placeholder="IDENTITY.md">
342
227
  </label>
343
- <label>proposers <span class="muted">(model refs, comma-separated)</span>
344
- <input type="text" name="refinement_proposers" value="<%= agent_list(rf["proposers"]) %>" placeholder="deepseek/deepseek-v4-flash">
345
- </label>
228
+ <div>proposers <span class="muted">(model refs)</span>
229
+ <%== model_slots("refinement_proposers_refs", rf["proposers"], min: 2) %>
230
+ </div>
346
231
  </div>
347
232
  <label>budget (tokens)
348
233
  <input type="text" name="refinement_budget_tokens" value="<%= rfb["tokens"] %>" inputmode="numeric">
@@ -351,79 +236,27 @@
351
236
  </div>
352
237
 
353
238
  <div class="config-group" data-config-group="budget_rel"<%== " hidden" unless @config_group == "budget_rel" %>>
354
- <fieldset class="subgroup">
355
- <legend>budget <span class="muted">— WS2: calendar spend caps per (tenant, agent)</span></legend>
356
- <% bd = @agent.budget || {} %>
357
- <div class="grid-3">
358
- <label>daily (tokens)
359
- <input type="text" name="budget_daily" value="<%= bd["daily"] %>" inputmode="numeric">
360
- </label>
361
- <label>monthly (tokens)
362
- <input type="text" name="budget_monthly" value="<%= bd["monthly"] %>" inputmode="numeric">
363
- </label>
364
- <label>alert at <span class="muted">(0–1 of the cap)</span>
365
- <input type="text" name="budget_alert_at" value="<%= bd["alert_at"] %>" placeholder="0.8" inputmode="decimal">
366
- </label>
367
- </div>
368
- <label class="check">
369
- <input type="checkbox" name="budget_soft" value="1"<%== " checked" if bd["soft"] %>>
370
- soft cap <span class="muted">— crossing it warns and runs; hard (default) fails the turn</span>
371
- </label>
372
- </fieldset>
373
-
374
- <fieldset class="subgroup">
375
- <legend>reliability <span class="muted">— WS3: retries, fallback chain, circuit breaker</span></legend>
376
- <% rel = @agent.reliability || {}; brk = rel["circuit_breaker"] || {} %>
377
- <div class="grid-3">
378
- <label>retries
379
- <input type="text" name="reliability_retries" value="<%= rel["retries"] %>" inputmode="numeric">
380
- </label>
381
- <label>backoff
382
- <select name="reliability_backoff">
383
- <option value="">none</option>
384
- <% %w[constant exponential].each do |m| %>
385
- <option value="<%= m %>"<%== ' selected' if rel["backoff"] == m %>><%= m %></option>
386
- <% end %>
387
- </select>
388
- </label>
389
- <label>timeout (s)
390
- <input type="text" name="reliability_timeout" value="<%= rel["timeout"] %>" placeholder="30" inputmode="numeric">
391
- </label>
392
- <label>breaker after <span class="muted">(failures)</span>
393
- <input type="text" name="reliability_breaker_after" value="<%= brk["after"] %>" inputmode="numeric">
394
- </label>
395
- <label>breaker within (s)
396
- <input type="text" name="reliability_breaker_within" value="<%= brk["within"] %>" inputmode="numeric">
397
- </label>
398
- <label>breaker cooldown (s)
399
- <input type="text" name="reliability_breaker_cooldown" value="<%= brk["cooldown"] %>" inputmode="numeric">
400
- </label>
401
- </div>
402
- <label>fallback models <span class="muted">(comma-separated refs, tried after retries)</span>
403
- <input type="text" name="reliability_fallback" value="<%= agent_list(rel["fallback"]) %>" placeholder="openai/gpt-4o-mini">
404
- </label>
405
- </fieldset>
239
+ <%== render("_cfg_budget") %>
240
+ <%== render("_cfg_reliability") %>
406
241
  </div>
407
242
 
408
243
  <div class="config-group" data-config-group="routing"<%== " hidden" unless @config_group == "routing" %>>
409
244
  <fieldset class="subgroup">
410
245
  <legend>routing, media, alerts &amp; the edge</legend>
411
- <label>routes <span class="muted">— WS4 JSON: intent → description / delegate / stuck (blank = no routing)</span>
246
+ <label>routes <span class="muted">— JSON: intent → description / delegate / stuck (blank = no routing)</span>
412
247
  <textarea name="routes" rows="4" spellcheck="false" class="mono" placeholder='{ "shopping": "the customer wants to browse", "default": "shopping" }'><%= agent_json(@agent.routes) %></textarea>
413
248
  </label>
414
- <label>outputs <span class="muted">— WS9 JSON: generated media the agent may produce (blank = none)</span>
249
+ <label>outputs <span class="muted">— JSON: generated media the agent may produce (blank = none)</span>
415
250
  <textarea name="outputs" rows="3" spellcheck="false" class="mono" placeholder='{ "image": { "model": "gpt-image-1" } }'><%= agent_json(@agent.outputs) %></textarea>
416
251
  </label>
417
252
  <label>STT vocabulary prompt <span class="muted">— domain words (products, brands) expected in this agent's voice notes (blank = deployment default)</span>
418
- <input type="text" name="stt_prompt" value="<%= @agent.stt_prompt %>" placeholder="Ocean Drop, tênis, boné trucker, chinelo">
253
+ <input type="text" name="stt_prompt" value="<%= @agent.stt_prompt %>" placeholder="Acme Surf, tênis, boné trucker, chinelo">
419
254
  </label>
420
255
  <div class="grid-2">
421
- <label>alerts webhook <span class="muted">(budget_warning / breaker_open / delivery_failed)</span>
422
- <input type="text" name="alerts_webhook" value="<%= @agent.alerts && @agent.alerts["webhook"] %>" placeholder="https://ops.example.com/insika-alerts">
423
- </label>
256
+ <%== render("_cfg_alerts") %>
424
257
  <label class="check">
425
258
  <input type="checkbox" name="stuck_signal" value="1"<%== " checked" if @agent.stuck_signal %>>
426
- stuck signal <span class="muted">— the model may end a turn <code>stuck</code> (WS5)</span>
259
+ stuck signal <span class="muted">— the model may end a turn <code>stuck</code></span>
427
260
  </label>
428
261
  </div>
429
262
  <div class="grid-2">
@@ -471,6 +304,9 @@
471
304
  <label>approvals required <span class="muted">(tools whose call suspends for a human)</span>
472
305
  <input type="text" name="approvals_required" value="<%= agent_list(@agent.approvals_required) %>" placeholder="send_discount_coupon">
473
306
  </label>
307
+ <label>customer confirm <span class="muted">(tools held until the customer says yes on the next message)</span>
308
+ <input type="text" name="customer_confirm" value="<%= agent_list(@agent.customer_confirm) %>" placeholder="create_order">
309
+ </label>
474
310
  <label>policies <span class="muted">(registry names)</span>
475
311
  <input type="text" name="policies" value="<%= agent_list(@agent.policies) %>" placeholder="tool_allowlist, skill_allowlist">
476
312
  </label>
@@ -1,8 +1,63 @@
1
1
  <!-- ── History ───────────────────────────────────────────────────────────── -->
2
2
  <section class="section" id="history" data-tabs-target="panel">
3
+ <div class="section-head">
4
+ <h2>What changed, and what it did</h2>
5
+ <p class="muted"><%= @change_primary ? "Primary outcome · #{@change_primary}" : "All outcomes" %> per day, last <%= @change_days.size %> days. A dashed line marks each change to a prompt file or to one of this agent's skills.</p>
6
+ </div>
7
+ <div class="card">
8
+ <% max = [@change_days.values.max || 0, 1].max; span = @change_days.size * 86_400.0 %>
9
+ <svg class="barchart" viewBox="0 0 700 176" preserveAspectRatio="none" role="img" aria-label="outcomes per day, last <%= @change_days.size %> days, with prompt-file changes">
10
+ <% slot = 700.0 / @change_days.size; bw = slot * 0.58 %>
11
+ <% @change_days.each_with_index do |(d, c), i| %>
12
+ <% h = (c.to_f / max) * 150.0; x = (i * slot) + ((slot - bw) / 2.0) %>
13
+ <rect class="bar<%== ' bar-empty' if c.zero? %>" x="<%= format('%.1f', x) %>" y="<%= format('%.1f', c.zero? ? 157.0 : 160.0 - h) %>"
14
+ width="<%= format('%.1f', bw) %>" height="<%= format('%.1f', c.zero? ? 3.0 : [h, 3.0].max) %>" rx="2">
15
+ <title><%= d.strftime('%b %-d') %> · <%= c %> outcome<%= 's' unless c == 1 %></title>
16
+ </rect>
17
+ <% end %>
18
+ <% @changes.each do |ch| %>
19
+ <% x = format('%.1f', (ch[:at] - @change_start) / span * 700.0) %>
20
+ <line class="change-mark" x1="<%= x %>" y1="0" x2="<%= x %>" y2="160" vector-effect="non-scaling-stroke"><title><%= ch[:kind] == :skill ? "skill " : "" %><%= ch[:file] %> · <%= ch[:at].strftime('%b %-d %H:%M UTC') %></title></line>
21
+ <% end %>
22
+ <line class="barchart-axis-line" x1="0" y1="160" x2="700" y2="160" />
23
+ </svg>
24
+ <div class="barchart-axis"><span><%= @change_days.keys.first.strftime('%b %-d') %></span><span>today</span></div>
25
+ </div>
26
+ <% if @changes.empty? %>
27
+ <div class="empty">No prompt or skill changes in the last <%= @change_days.size %> days.</div>
28
+ <% end %>
29
+ <% @changes.each do |ch| %>
30
+ <div class="card">
31
+ <div class="drill-section-head">
32
+ <h3><% if ch[:kind] == :skill %><span class="muted">skill</span> <% end %><%= ch[:file] %></h3>
33
+ <span class="muted mono"><%= ch[:at].strftime('%Y-%m-%d %H:%M UTC') %></span>
34
+ </div>
35
+ <% unless ch[:removed].empty? && ch[:added].empty? %>
36
+ <pre class="diff"><% ch[:removed].first(6).each do |l| %><span class="del">- <%= l %></span><% end %><% ch[:added].first(6).each do |l| %><span class="ins">+ <%= l %></span><% end %></pre>
37
+ <% end %>
38
+ <p>
39
+ <% if ch[:before] && ch[:after] %>
40
+ Outcomes: <strong class="tnum"><%= ch[:after] %></strong> in the 7 days after vs <strong class="tnum"><%= ch[:before] %></strong> in the 7 days before.
41
+ <% else %>
42
+ <span class="muted">Outcomes 7 days after vs 7 days before: not enough data.</span>
43
+ <% end %>
44
+ </p>
45
+ <% if ch[:kind] == :skill %>
46
+ <a href="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/skills/<%= Rack::Utils.escape(ch[:file]) %>" data-turbo-frame="_top">Open the skill</a>
47
+ <% else %>
48
+ <form method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/prompts/restore">
49
+ <%== csrf_tag %>
50
+ <input type="hidden" name="file" value="<%= ch[:file] %>">
51
+ <input type="hidden" name="version" value="<%= ch[:version] %>">
52
+ <button type="submit" class="ghost">Restore the version before</button>
53
+ </form>
54
+ <% end %>
55
+ </div>
56
+ <% end %>
57
+
3
58
  <div class="section-head">
4
59
  <h2>History</h2>
5
- <p class="muted">recent conversations (all agents — a session doesn't stamp its author).</p>
60
+ <p class="muted">This agent's most recent conversations. Every conversation is in <a href="/studio/chats?agent=<%= Rack::Utils.escape(@agent.id) %>" data-turbo-frame="_top">Chats</a>.</p>
6
61
  </div>
7
62
  <% if @recent_sessions.empty? %>
8
63
  <div class="empty">No conversations yet. Start one in the <a href="/studio/playground?agent=<%= Rack::Utils.escape(@agent.id) %>" data-turbo-frame="_top">playground</a>.</div>
@@ -0,0 +1,34 @@
1
+ <!-- ── Performance ───────────────────────────────────────────────────────── -->
2
+ <% periods = { "24h" => "24h", "7d" => "7 days", "30d" => "30 days" } %>
3
+ <% q = Rack::Utils.escape(@agent.id) %>
4
+ <section class="section" id="performance" data-tabs-target="panel">
5
+ <div class="section-head">
6
+ <h2>Performance</h2>
7
+ <p class="muted">The same numbers as the <a href="/studio/home?agent=<%= q %>" data-turbo-frame="_top">home</a> and <a href="/studio/models?agent=<%= q %>" data-turbo-frame="_top">Models</a>, for this agent. Latency is the whole turn; first balloon is what a channel customer waits.</p>
8
+ </div>
9
+ <div class="card">
10
+ <table class="grid">
11
+ <thead><tr><th></th><% periods.each_value do |label| %><th class="num"><%= label %></th><% end %></tr></thead>
12
+ <tbody>
13
+ <% pct = ->(ms) { ms.nil? ? "—" : format("%.1fs", ms / 1000.0) } %>
14
+ <% rows = [
15
+ ["Conversations", ->(p) { @perf_activity[p][:conversations] }],
16
+ ["Messages", ->(p) { @perf_activity[p][:messages] }],
17
+ ["Outcomes", ->(p) { @perf_activity[p][:outcomes] }],
18
+ ["Cost", ->(p) { usd(@perf.dig(p, "totals", "cost")) }],
19
+ ["Model requests", ->(p) { @perf.dig(p, "totals", "requests") || 0 }],
20
+ ["Tokens in / out", ->(p) { t = @perf.dig(p, "totals") || {}; "#{t['input_tokens'] || 0} / #{t['output_tokens'] || 0}" }],
21
+ ["Turns", ->(p) { @perf.dig(p, "turns", "count") || 0 }],
22
+ ["Failed turns", ->(p) { t = @perf.dig(p, "turns"); t && t["count"].positive? ? format("%d (%.1f%%)", t["failures"], t["failures"] * 100.0 / t["count"]) : "—" }],
23
+ ["ttft p50 / p95", ->(p) { w = @perf.dig(p, "turns", "windows", "ttft_ms") || {}; "#{pct.(w['p50'])} / #{pct.(w['p95'])}" }],
24
+ ["Turn p50 / p95", ->(p) { w = @perf.dig(p, "turns", "windows", "total_ms") || {}; "#{pct.(w['p50'])} / #{pct.(w['p95'])}" }],
25
+ ["First balloon p95", ->(p) { pct.(@perf.dig(p, "turns", "windows", "first_balloon_ms", "p95")) }],
26
+ ["Tool calls ok", ->(p) { tools = Array(@perf.dig(p, "turns", "tools")); calls = tools.sum { |t| t["calls"] }; calls.zero? ? "—" : format("%.1f%% of %d", (calls - tools.sum { |t| t["failures"] }) * 100.0 / calls, calls) }]
27
+ ] %>
28
+ <% rows.each do |label, value| %>
29
+ <tr><td><%= label %></td><% periods.each_key do |p| %><td class="num"><%= value.(p) %></td><% end %></tr>
30
+ <% end %>
31
+ </tbody>
32
+ </table>
33
+ </div>
34
+ </section>
@@ -2,7 +2,7 @@
2
2
  <section class="section" id="prompts" data-tabs-target="panel">
3
3
  <div class="section-head">
4
4
  <h2>Prompts <span class="pill info plain"><%= @prompt_files.size %></span></h2>
5
- <p class="muted">the agent's own identity (store-backed, versioned). Empty = uses the deployment default.</p>
5
+ <p class="muted">Define this agent’s identity and voice. Save each prompt separately; no files means the deployment default is used.</p>
6
6
  </div>
7
7
 
8
8
  <div class="drill" data-controller="list-filter">
@@ -60,19 +60,17 @@
60
60
  <% else %>
61
61
  <input type="hidden" name="file" value="<%= @prompt_selected %>">
62
62
  <% end %>
63
- <label>content <span class="muted">(markdown — the agent's identity/voice)</span></label>
63
+ <label for="prompt-content">content <span class="muted">(markdown — the agent's identity/voice)</span></label>
64
64
  <div data-controller="code-editor" data-code-editor-language-value="markdown" data-code-editor-preview-value="true">
65
- <textarea name="content" data-code-editor-target="source" rows="18"><%= new ? "" : (pf ? pf[:content] : "") %></textarea>
65
+ <textarea id="prompt-content" name="content" data-code-editor-target="source" rows="18"><%= new ? "" : (pf ? pf[:content] : "") %></textarea>
66
66
  </div>
67
67
  </form>
68
68
 
69
69
  <% if !new && pf %>
70
70
  <% unless pf[:versions].empty? %>
71
- <div class="drill-section">
72
- <div class="drill-section-head">
73
- <h3>Versions</h3>
74
- <span class="muted">store-backed history — restore replaces the current content.</span>
75
- </div>
71
+ <details class="drill-section newfile">
72
+ <summary>Previous versions <span class="count"><%= pf[:versions].size %></span></summary>
73
+ <p class="muted">Restore replaces the current content.</p>
76
74
  <ul class="versions-list">
77
75
  <% pf[:versions].each_with_index do |v, i| %>
78
76
  <li>
@@ -86,7 +84,7 @@
86
84
  </li>
87
85
  <% end %>
88
86
  </ul>
89
- </div>
87
+ </details>
90
88
  <% end %>
91
89
 
92
90
  <div class="drill-section">