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
@@ -45,11 +45,36 @@ module Studio
45
45
  end
46
46
  smm = edge_int(r.params["steer_max_messages"], "steer_max_messages")
47
47
  smm.nil? ? limits.delete(:steer_max_messages) : limits[:steer_max_messages] = smm
48
+ # The debounce window is what makes `collect`/`steer` merge a burst AT THE
49
+ # DOOR; without it a steer agent only ever absorbs mid-run, at a tool
50
+ # boundary, and a turn that never calls one absorbs nothing. Same opt-in
51
+ # convention as above: blank DELETES, 0 = no window.
52
+ %w[debounce_ms debounce_max_ms].each do |field|
53
+ v = edge_int(r.params[field], field)
54
+ v.nil? ? limits.delete(field.to_sym) : limits[field.to_sym] = v
55
+ end
56
+ # Refused HERE (config time) and not at resolve time: a template without
57
+ # the placeholder drops the customer's message while the turn still looks
58
+ # steered — the same rule QueuePolicy#join! enforces, moved to where the
59
+ # operator is.
60
+ if (join = presence(r.params["steer_join"]))
61
+ unless join.include?(Insika::QueuePolicy::JOIN_PLACEHOLDER)
62
+ raise Insika::ValidationError,
63
+ "steer_join must contain #{Insika::QueuePolicy::JOIN_PLACEHOLDER}"
64
+ end
65
+
66
+ limits[:steer_join] = join
67
+ else
68
+ limits.delete(:steer_join)
69
+ end
70
+ model, provider = picked_model(r, model_ref_of(@agent.model, @agent.provider))
48
71
  {
49
72
  id: @agent.id,
50
- model: presence(r.params["model"]),
51
- provider: r.params["provider"].to_s,
73
+ model: model,
74
+ provider: provider.to_s,
52
75
  memory: r.params["memory"] == "1",
76
+ memory_retrieval: memory_retrieval_patch(r),
77
+ knowledge: knowledge_patch(r),
53
78
  limits: limits,
54
79
  params: params_patch(r),
55
80
  model_policy: model_policy_patch(r),
@@ -85,11 +110,99 @@ module Studio
85
110
  context_providers: allowlist_patch(r, "context_providers"),
86
111
  workflows_allow: allowlist_patch(r, "workflows_allow"),
87
112
  approvals_required: list_patch(r, "approvals_required"),
113
+ customer_confirm: list_patch(r, "customer_confirm"),
88
114
  policies: list_patch(r, "policies").map(&:to_sym),
89
115
  prompt_refs: list_patch(r, "prompt_refs"),
90
116
  capabilities_declared: list_patch(r, "capabilities_declared"),
91
117
  skills_eager: skills_eager_patch(r)
92
- }
118
+ }.merge(inherited_fields(r))
119
+ end
120
+
121
+ # The Studio picker posts `model_ref` (checked against the list; `kept` = the
122
+ # stored ref, allowed back unchanged). Without it, the plain `model`/`provider`
123
+ # pair passes as given (API clients, older forms). -> [model | nil, provider | nil]
124
+ def picked_model(r, kept = nil)
125
+ return model_ref_parts(r.params["model"], r.params["provider"]) unless r.params.key?("model_ref")
126
+
127
+ model_ref_parts(checked_model_ref(r.params["model_ref"], kept), nil)
128
+ end
129
+
130
+ # Any other model field the Studio renders as a picker posts "<field>_ref" (one
131
+ # ref) or "<field>_refs[]" (one per slot), checked like the agent model; `kept`
132
+ # (the stored value) may come back unchanged. The plain field passes as given.
133
+ def ref_param(r, field, kept)
134
+ key = "#{field}_ref"
135
+ r.params.key?(key) ? checked_model_ref(r.params[key], kept) : presence(r.params[field])
136
+ end
137
+
138
+ def ref_list_param(r, field, kept)
139
+ key = "#{field}_refs"
140
+ return list(r.params[field]) unless r.params.key?(key)
141
+
142
+ Array(r.params[key]).filter_map { |ref| checked_model_ref(ref, Array(kept)) }
143
+ end
144
+
145
+ # ONE ref, "provider/model" (an OpenRouter model keeps its own slash:
146
+ # "openrouter/deepseek/x"). A separate `provider` wins as given.
147
+ def model_ref_parts(ref, provider)
148
+ model = presence(ref)
149
+ return [model, presence(provider)] if model.nil? || !presence(provider).nil? || !model.include?("/")
150
+
151
+ provider, model = model.split("/", 2)
152
+ [model, provider]
153
+ end
154
+
155
+ # The sections whose "use the platform default" box is checked save nil, so
156
+ # the agent keeps inheriting (Insika::AgentDefaults).
157
+ def inherited_fields(r)
158
+ (Array(r.params["inherit"]) & Insika::AgentDefaults::FIELDS).to_h { |field| [field.to_sym, nil] }
159
+ end
160
+
161
+ # The platform agent defaults form: the same sections and parsers as the
162
+ # agent form, read against the current defaults (so pack-only keys survive).
163
+ def agent_defaults_patch(r)
164
+ @agent = agent_defaults_profile
165
+ Insika::AgentDefaults::FIELDS.to_h { |field| [field, public_send(:"#{field}_patch", r)] }
166
+ end
167
+
168
+ def memory_retrieval_patch(r)
169
+ rerank = retrieval_rerank_patch(r, "memory")
170
+ return nil unless rerank
171
+
172
+ { "top_k" => retrieval_top_k(r, "memory"), "rerank" => rerank }
173
+ end
174
+
175
+ def knowledge_patch(r)
176
+ # Preserve pack options the form does not expose, such as the index.
177
+ out = (@agent.knowledge || {}).except("extract", "retrieve", "top_k", "types", "model", "prompt", "rerank")
178
+ out.merge!("extract" => r.params["knowledge_extract"] == "1",
179
+ "retrieve" => r.params["knowledge_retrieve"] == "1",
180
+ "top_k" => retrieval_top_k(r, "knowledge"))
181
+ types = list(r.params["knowledge_types"])
182
+ out["types"] = types unless types.empty?
183
+ (m = ref_param(r, "knowledge_model", (@agent.knowledge || {})["model"])) && (out["model"] = m)
184
+ (p = presence(r.params["knowledge_prompt"])) && (out["prompt"] = p)
185
+ rerank = retrieval_rerank_patch(r, "knowledge")
186
+ out["rerank"] = rerank if rerank
187
+ out
188
+ end
189
+
190
+ def retrieval_top_k(r, source)
191
+ field = "#{source}_top_k"
192
+ value = edge_int(r.params[field], field) || 5
193
+ raise Insika::ValidationError, "#{field} must be positive" unless value.positive?
194
+
195
+ value
196
+ end
197
+
198
+ def retrieval_rerank_patch(r, source)
199
+ return nil unless r.params["#{source}_rerank_enabled"] == "1"
200
+
201
+ prefix = "#{source}_rerank"
202
+ { "provider" => presence(r.params["#{prefix}_provider"]),
203
+ "model" => presence(r.params["#{prefix}_model"]),
204
+ "candidate_limit" => edge_int(r.params["#{prefix}_candidate_limit"], "#{prefix}_candidate_limit") || 20,
205
+ "timeout_seconds" => edge_float(r.params["#{prefix}_timeout_seconds"], "#{prefix}_timeout_seconds") || 2 }
93
206
  end
94
207
 
95
208
  # grounding — { "mode" => "flag"|"enforce", "matcher" =>
@@ -162,7 +275,7 @@ module Studio
162
275
  enabled = r.params["distill_enabled"] == "1"
163
276
  out = { "enabled" => enabled }
164
277
  (p = presence(r.params["distill_prompt"])) && (out["prompt"] = p)
165
- (m = presence(r.params["distill_model"])) && (out["model"] = m)
278
+ (m = ref_param(r, "distill_model", (@agent.distill || {})["model"])) && (out["model"] = m)
166
279
  %w[idle_hours min_messages max_proposals].each do |key|
167
280
  v = edge_int(r.params["distill_#{key}"], "distill_#{key}")
168
281
  out[key] = v unless v.nil?
@@ -189,7 +302,7 @@ module Studio
189
302
 
190
303
  def harvest_miner_patch(r)
191
304
  miner = {}
192
- (m = presence(r.params["harvest_miner_model"])) && (miner["model"] = m)
305
+ (m = ref_param(r, "harvest_miner_model", (@agent.harvest || {}).dig("miner", "model"))) && (miner["model"] = m)
193
306
  window = {}
194
307
  (w = edge_int(r.params["harvest_miner_window"], "harvest_miner_window")) && (window["last_sessions"] = w)
195
308
  miner["window"] = window unless window.empty?
@@ -210,7 +323,7 @@ module Studio
210
323
  (f = edge_int(r.params["refinement_max_findings"], "refinement_max_findings")) && (out["max_findings"] = f)
211
324
  files = list(r.params["refinement_files"])
212
325
  out["files"] = files unless files.empty?
213
- proposers = list(r.params["refinement_proposers"])
326
+ proposers = ref_list_param(r, "refinement_proposers", (@agent.refinement || {})["proposers"])
214
327
  out["proposers"] = proposers unless proposers.empty?
215
328
  (t = edge_int(r.params["refinement_budget_tokens"], "refinement_budget_tokens")) && (out["budget"] = { "tokens" => t })
216
329
  (e = edge_int(r.params["refinement_auto_apply_max_edits"], "refinement_auto_apply_max_edits")) && (out["auto_apply_max_edits"] = e)
@@ -237,7 +350,7 @@ module Studio
237
350
  retries = edge_int(r.params["reliability_retries"], "reliability_retries")
238
351
  backoff = presence(r.params["reliability_backoff"])
239
352
  timeout = edge_int(r.params["reliability_timeout"], "reliability_timeout")
240
- fallback = list(r.params["reliability_fallback"])
353
+ fallback = ref_list_param(r, "reliability_fallback", (@agent.reliability || {})["fallback"])
241
354
  return nil if retries.nil? && backoff.nil? && timeout.nil? && fallback.empty?
242
355
 
243
356
  out = {}
@@ -328,7 +441,8 @@ module Studio
328
441
  out = {
329
442
  "input" => r.params["guardrail_input"] == "1",
330
443
  "output" => r.params["guardrail_output"] == "1",
331
- "strictness" => presence(r.params["guardrail_strictness"]) || "medium"
444
+ "strictness" => presence(r.params["guardrail_strictness"]) || "medium",
445
+ "injection" => r.params["guardrail_injection"] == "block" ? "block" : "flag"
332
446
  }
333
447
  (mod = presence(r.params["guardrail_moderator"])) && (out["moderator"] = mod)
334
448
  responses = guardrail_responses_patch(r)
@@ -462,6 +576,10 @@ module Studio
462
576
  end
463
577
  end
464
578
 
579
+ def stored_compaction_model
580
+ insika[:settings_store] ? insika[:settings_store].get.dig("compaction", "model") : nil
581
+ end
582
+
465
583
  def settings_patch(r)
466
584
  patch = {
467
585
  "streaming" => r.params["streaming"] == "1"
@@ -481,7 +599,7 @@ module Studio
481
599
  # when cleared (deep_merge — the defaults backstop a fresh record);
482
600
  # model blank = nil = the platform utility_model.
483
601
  compaction = { "enabled" => r.params["compaction_enabled"] == "1",
484
- "model" => presence(r.params["compaction_model"]) }
602
+ "model" => ref_param(r, "compaction_model", stored_compaction_model) }
485
603
  %w[keep_last compact_after].each do |f|
486
604
  v = presence(r.params["compaction_#{f}"])
487
605
  compaction[f] = Integer(v) if v
@@ -503,6 +621,30 @@ module Studio
503
621
  { "edge" => edge }
504
622
  end
505
623
 
624
+ # Burst policy — the PLATFORM layer of the QueuePolicy, saved from its own form
625
+ # (a sub-resource, like edge/models). Every field writes nil when blank, and nil
626
+ # here means "no platform default" (QueuePolicy.pick skips it) — NOT 0, which on
627
+ # `steer_max_messages` would read as "never steer" deploy-wide. The two refusals
628
+ # are the agent form's, verbatim: an unknown mode and a `steer_join` that would
629
+ # drop the customer's message are config errors, caught where the operator is.
630
+ def queue_patch(r)
631
+ mode = presence(r.params["queue_mode"])
632
+ if mode && !Insika::QueuePolicy::MODES.map(&:to_s).include?(mode)
633
+ raise Insika::ValidationError, "queue_mode must be one of: #{Insika::QueuePolicy::MODES.join(', ')}"
634
+ end
635
+
636
+ join = presence(r.params["steer_join"])
637
+ if join && !join.include?(Insika::QueuePolicy::JOIN_PLACEHOLDER)
638
+ raise Insika::ValidationError, "steer_join must contain #{Insika::QueuePolicy::JOIN_PLACEHOLDER}"
639
+ end
640
+
641
+ queue = { "queue_mode" => mode, "steer_join" => join }
642
+ %w[debounce_ms debounce_max_ms steer_max_messages].each do |f|
643
+ queue[f] = edge_int(r.params[f], f)
644
+ end
645
+ { "queue" => queue }
646
+ end
647
+
506
648
  # An eval case arrives as the YAML text the operator edited — the same shape the
507
649
  # corpus files hold, so there is one format to learn and a pull request can review
508
650
  # what was authored. Decoding happens HERE, at the transport edge; the SHAPE is
@@ -573,7 +715,27 @@ module Studio
573
715
  # deep-merge writes and the ModelResolver reads as "no platform default");
574
716
  # fallback_models is a full-list replace (deep_merge substitutes arrays, so a
575
717
  # resubmit never accumulates).
718
+ # The pickers post `*_ref` fields, each checked against the list (a stored
719
+ # value may come back unchanged): default_model splits into model/provider like
720
+ # the agent's; fallback_refs[] is one picker per slot (blank slots dropped).
721
+ # Without them, the plain fields pass as given (API clients, older forms).
576
722
  def model_defaults_patch(r)
723
+ return plain_model_defaults(r) unless r.params.key?("default_model_ref")
724
+
725
+ current = insika[:settings_store] ? insika[:settings_store].get : {}
726
+ kept_default = model_ref_of(current["default_model"], current["default_provider"])
727
+ default_model, default_provider = model_ref_parts(checked_model_ref(r.params["default_model_ref"], kept_default), nil)
728
+ kept_fallbacks = Array(current["fallback_models"])
729
+ {
730
+ "default_model" => default_model,
731
+ "default_provider" => default_provider,
732
+ "utility_model" => checked_model_ref(r.params["utility_model_ref"], current["utility_model"]),
733
+ "fallback_models" => Array(r.params["fallback_refs"]).filter_map { |f| checked_model_ref(f, kept_fallbacks) },
734
+ "thinking" => presence(r.params["thinking"])
735
+ }
736
+ end
737
+
738
+ def plain_model_defaults(r)
577
739
  {
578
740
  "default_model" => presence(r.params["default_model"]),
579
741
  "default_provider" => presence(r.params["default_provider"]),
@@ -8,13 +8,16 @@ module Studio
8
8
  # 20×20 stroke icons (currentColor), one per nav key. Source: a Lucide-style
9
9
  # line icon set.
10
10
  NAV_ICONS = {
11
+ search: '<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/>',
11
12
  home: '<path d="M3 9.5 12 3l9 6.5"/><path d="M5 10v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V10"/><path d="M9 21v-6h6v6"/>',
12
13
  agents: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
13
14
  skills: '<path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z"/>',
15
+ kits: '<path d="M21 8 12 3 3 8v8l9 5 9-5z"/><path d="m3 8 9 5 9-5"/><path d="M12 13v8"/>',
14
16
  tools: '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>',
15
17
  system: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8"/><path d="M8 17h8"/>',
16
18
  mcp: '<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/>',
17
19
  settings: '<path d="M20 7h-9"/><path d="M14 17H5"/><circle cx="17" cy="17" r="3"/><circle cx="7" cy="7" r="3"/>',
20
+ live: '<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>',
18
21
  chats: '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
19
22
  playground: '<polygon points="6 3 20 12 6 21 6 3"/>',
20
23
  tasks: '<path d="M11 12H3"/><path d="M16 6H3"/><path d="M16 18H3"/><path d="m18 9 3 3-3 3"/>',
@@ -0,0 +1,73 @@
1
+ <% knowledge = @agent.knowledge || {} %>
2
+ <p class="muted">Memory keeps facts and notes across conversations. Knowledge stores reusable concepts. Reranking only changes which saved information is selected for a turn.</p>
3
+ <% [["memory", "Memory", @agent.memory_retrieval || {}], ["knowledge", "Knowledge", knowledge]].each do |source, title, config| %>
4
+ <fieldset class="subgroup">
5
+ <legend><%= title %></legend>
6
+ <%== inherit_toggle(source == "memory" ? "memory_retrieval" : "knowledge") %>
7
+ <% if source == "memory" %>
8
+ <% unless @agent_defaults_form %>
9
+ <label class="check own">
10
+ <input type="checkbox" name="memory" value="1"<%== " checked" if @agent.memory %>>
11
+ Remember across conversations
12
+ </label>
13
+ <% end %>
14
+ <p class="muted">Without reranking, memory uses saved facts and the ten most recent notes. Turning memory off does not delete them.</p>
15
+ <% else %>
16
+ <label class="check">
17
+ <input type="checkbox" name="knowledge_extract" value="1"<%== " checked" if Insika::Coercion.truthy?(knowledge["extract"]) %>>
18
+ Learn concepts from conversations
19
+ </label>
20
+ <label class="check">
21
+ <input type="checkbox" name="knowledge_retrieve" value="1"<%== " checked" if Insika::Coercion.truthy?(knowledge["retrieve"]) %>>
22
+ Use knowledge in replies
23
+ </label>
24
+ <p class="muted">Learning and retrieval work independently. Turning them off does not delete saved concepts.</p>
25
+ <label>Concepts selected per turn
26
+ <input type="number" name="knowledge_top_k" value="<%= knowledge.fetch("top_k", 5) %>" min="1" step="1">
27
+ </label>
28
+ <details class="newfile">
29
+ <summary>Knowledge learning settings</summary>
30
+ <div class="grid-2">
31
+ <label>Concept types <span class="muted">— comma-separated; blank = default types</span>
32
+ <input type="text" name="knowledge_types" value="<%= Array(knowledge["types"]).join(", ") %>" placeholder="fact, policy, objection">
33
+ </label>
34
+ <label>Learning model <span class="muted">— blank = platform utility model</span>
35
+ <%== model_select("knowledge_model_ref", knowledge["model"], "platform utility model") %>
36
+ </label>
37
+ </div>
38
+ <label>Learning instructions <span class="muted">— blank = default instructions</span>
39
+ <textarea name="knowledge_prompt" rows="4"><%= knowledge["prompt"] %></textarea>
40
+ </label>
41
+ </details>
42
+ <% end %>
43
+ <% rerank = config["rerank"] || {} %>
44
+ <label class="check">
45
+ <input type="checkbox" name="<%= source %>_rerank_enabled" value="1"<%== " checked" if config["rerank"] %>>
46
+ Rerank <%= source %> by relevance
47
+ </label>
48
+ <p class="muted">Off by default. Adds a provider call and may increase cost. Requires <%= source == "memory" ? "memory" : "knowledge retrieval" %> to be on. Provider credentials are configured separately.</p>
49
+ <details class="newfile"<%== " open" if config["rerank"] %>>
50
+ <summary><%= title %> rerank settings</summary>
51
+ <div class="grid-2">
52
+ <label>Rerank provider
53
+ <input type="text" name="<%= source %>_rerank_provider" value="<%= rerank["provider"] %>" placeholder="cohere">
54
+ </label>
55
+ <label>Rerank model
56
+ <input type="text" name="<%= source %>_rerank_model" value="<%= rerank["model"] %>" placeholder="rerank-v3.5">
57
+ </label>
58
+ <% if source == "memory" %>
59
+ <label>Memory items selected per turn
60
+ <input type="number" name="memory_top_k" value="<%= config.fetch("top_k", 5) %>" min="1" max="100" step="1">
61
+ </label>
62
+ <% end %>
63
+ <label>Candidate limit <span class="muted">— at least the selected count, up to 100</span>
64
+ <input type="number" name="<%= source %>_rerank_candidate_limit" value="<%= rerank.fetch("candidate_limit", 20) %>" min="1" max="100" step="1">
65
+ </label>
66
+ <label>Rerank timeout (seconds)
67
+ <input type="number" name="<%= source %>_rerank_timeout_seconds" value="<%= rerank.fetch("timeout_seconds", 2) %>" min="0.001" step="any">
68
+ </label>
69
+ </div>
70
+ <p class="muted">Choose a rerank model registered for this provider. On failure, the agent uses the usual retrieval. Disabling rerank clears these rerank settings.</p>
71
+ </details>
72
+ </fieldset>
73
+ <% end %>
@@ -0,0 +1,15 @@
1
+ <%# Type-to-search agent picker: an <input list> over a <datalist> — native
2
+ search in every browser, no JS. Locals: name, options (ids), value,
3
+ blank (placeholder meaning "none / all"; nil = a value is required),
4
+ auto_submit (submit the form when a pick is committed). The server still
5
+ validates the value: a typed id that is not an agent is refused there. %>
6
+ <%# unique per page: a page can carry two pickers (a filter and a form) %>
7
+ <% list_id = "agent-options-#{name}-#{@agent_picker_seq = (@agent_picker_seq || 0) + 1}" %>
8
+ <% blank = defined?(blank) ? blank : nil %>
9
+ <% auto_submit = defined?(auto_submit) && auto_submit %>
10
+ <input type="search" class="filter-input agent-picker" name="<%= name %>" list="<%= list_id %>" value="<%= value %>" autocomplete="off"
11
+ placeholder="<%= blank || "Type to search agents…" %>"<%== " required" unless blank %>
12
+ <%== %(data-controller="auto-submit" data-action="change->auto-submit#submit") if auto_submit %>>
13
+ <datalist id="<%= list_id %>">
14
+ <% Array(options).each do |id| %><option value="<%= id %>"></option><% end %>
15
+ </datalist>