insika 0.1.0 → 0.3.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 (280) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +199 -5
  3. data/README.md +8 -2
  4. data/bin/insika +231 -13
  5. data/docs/AGENTS.md +505 -6
  6. data/docs/API.md +56 -0
  7. data/docs/CHANNELS.md +100 -10
  8. data/docs/CONTEXT.md +147 -19
  9. data/docs/DEPLOY.md +34 -11
  10. data/docs/EMBEDDING.md +11 -7
  11. data/docs/EVALS.md +20 -1
  12. data/docs/FACTS.md +135 -0
  13. data/docs/HARVEST.md +117 -0
  14. data/docs/LOADTEST.md +17 -10
  15. data/docs/OBSERVABILITY.md +65 -2
  16. data/docs/REFINEMENT.md +9 -9
  17. data/docs/RELEASING.md +34 -7
  18. data/docs/RUNNING-LOCAL.md +4 -4
  19. data/docs/SECURITY.md +85 -11
  20. data/docs/SKILLS.md +189 -3
  21. data/docs/SOAK.md +127 -0
  22. data/docs/TOOLS.md +70 -2
  23. data/docs/WHY.md +1 -1
  24. data/docs/WORKFLOWS.md +2 -2
  25. data/docs/domain.md +115 -0
  26. data/docs/index.md +2 -2
  27. data/docs/onboarding/start.md +1 -1
  28. data/lib/insika/agent_profile.rb +228 -26
  29. data/lib/insika/alert_dispatcher.rb +139 -0
  30. data/lib/insika/balloon_splitter.rb +102 -0
  31. data/lib/insika/baseline_store.rb +2 -2
  32. data/lib/insika/budget_ledger.rb +166 -0
  33. data/lib/insika/cache_series_store.rb +49 -0
  34. data/lib/insika/channel_delivery.rb +132 -24
  35. data/lib/insika/channel_registry.rb +1 -1
  36. data/lib/insika/channels/relay.rb +80 -6
  37. data/lib/insika/channels/web/widget.js +2 -2
  38. data/lib/insika/channels/web.rb +9 -9
  39. data/lib/insika/channels/webhook.rb +58 -0
  40. data/lib/insika/chat_builder.rb +145 -13
  41. data/lib/insika/checkpoint_store.rb +16 -0
  42. data/lib/insika/circuit_state.rb +114 -0
  43. data/lib/insika/coercion.rb +8 -0
  44. data/lib/insika/commands/agent_payload.rb +6 -4
  45. data/lib/insika/commands/cancel_followup.rb +49 -0
  46. data/lib/insika/commands/create_agent.rb +2 -2
  47. data/lib/insika/commands/create_session.rb +1 -1
  48. data/lib/insika/commands/delete_llm_provider.rb +1 -1
  49. data/lib/insika/commands/delete_skill.rb +43 -0
  50. data/lib/insika/commands/delete_tenant_data.rb +95 -0
  51. data/lib/insika/commands/export_customer_memory.rb +48 -0
  52. data/lib/insika/commands/forget_customer.rb +117 -0
  53. data/lib/insika/commands/freeze_funnel_baseline.rb +113 -0
  54. data/lib/insika/commands/gate_harvest.rb +138 -0
  55. data/lib/insika/commands/gate_refinement.rb +12 -12
  56. data/lib/insika/commands/import_mcp_tools.rb +1 -1
  57. data/lib/insika/commands/import_tools.rb +4 -4
  58. data/lib/insika/commands/issue_tenant_token.rb +41 -0
  59. data/lib/insika/commands/judge_shadow_pairs.rb +124 -0
  60. data/lib/insika/commands/memory_forget_fact.rb +20 -4
  61. data/lib/insika/commands/memory_put_fact.rb +23 -4
  62. data/lib/insika/commands/promote_harvest.rb +130 -0
  63. data/lib/insika/commands/record_outcome.rb +46 -0
  64. data/lib/insika/commands/record_shadow_reply.rb +68 -0
  65. data/lib/insika/commands/reject_harvest.rb +38 -0
  66. data/lib/insika/commands/resolve_proposal.rb +108 -0
  67. data/lib/insika/commands/resolve_refinement.rb +1 -1
  68. data/lib/insika/commands/revoke_contact.rb +49 -0
  69. data/lib/insika/commands/revoke_token.rb +39 -0
  70. data/lib/insika/commands/rollback_harvest.rb +86 -0
  71. data/lib/insika/commands/rotate_tenant_token.rb +43 -0
  72. data/lib/insika/commands/run_distillation.rb +186 -0
  73. data/lib/insika/commands/run_harvest.rb +393 -0
  74. data/lib/insika/commands/run_refinement.rb +5 -5
  75. data/lib/insika/commands/send_message.rb +112 -15
  76. data/lib/insika/commands/session_purge.rb +67 -0
  77. data/lib/insika/commands/set_agent_tools.rb +1 -1
  78. data/lib/insika/commands/set_skill_agents.rb +60 -19
  79. data/lib/insika/commands/trigger_workflow.rb +1 -1
  80. data/lib/insika/commands/update_agent.rb +1 -1
  81. data/lib/insika/commands/write_data_tool.rb +1 -1
  82. data/lib/insika/commands/write_golden.rb +1 -1
  83. data/lib/insika/commands/write_skill.rb +19 -9
  84. data/lib/insika/config_store.rb +8 -4
  85. data/lib/insika/contact_store.rb +183 -0
  86. data/lib/insika/context/builder.rb +23 -5
  87. data/lib/insika/context/fragment.rb +31 -3
  88. data/lib/insika/context/priority.rb +6 -2
  89. data/lib/insika/context/provider.rb +17 -3
  90. data/lib/insika/context/providers/briefing.rb +96 -0
  91. data/lib/insika/context/providers/memory.rb +16 -7
  92. data/lib/insika/context/providers/prompt.rb +30 -2
  93. data/lib/insika/context/providers/request.rb +1 -1
  94. data/lib/insika/context/providers/session.rb +17 -2
  95. data/lib/insika/context/providers/skill.rb +7 -1
  96. data/lib/insika/context/providers/skill_trigger.rb +128 -0
  97. data/lib/insika/context/providers/tool_search.rb +2 -0
  98. data/lib/insika/context_trace_store.rb +128 -0
  99. data/lib/insika/delegation_store.rb +2 -2
  100. data/lib/insika/distill.rb +224 -0
  101. data/lib/insika/distill_engine.rb +169 -0
  102. data/lib/insika/doctor.rb +962 -7
  103. data/lib/insika/dsl/runtime.rb +20 -11
  104. data/lib/insika/dsl/server_boot.rb +74 -4
  105. data/lib/insika/dsl/system.rb +1 -1
  106. data/lib/insika/dsl.rb +152 -15
  107. data/lib/insika/edge_limiter.rb +167 -8
  108. data/lib/insika/egress_guard.rb +3 -3
  109. data/lib/insika/env_schema.rb +22 -12
  110. data/lib/insika/errors.rb +72 -5
  111. data/lib/insika/evals/assertions.rb +15 -14
  112. data/lib/insika/evals/baseline.rb +3 -3
  113. data/lib/insika/evals/golden.rb +8 -8
  114. data/lib/insika/evals/judge.rb +7 -7
  115. data/lib/insika/evals/pairwise.rb +21 -9
  116. data/lib/insika/evals/report.rb +2 -2
  117. data/lib/insika/evals/runner.rb +6 -6
  118. data/lib/insika/evals/transport.rb +2 -2
  119. data/lib/insika/event_stream.rb +23 -5
  120. data/lib/insika/evidence.rb +183 -0
  121. data/lib/insika/executor.rb +1092 -160
  122. data/lib/insika/followup_engine.rb +207 -0
  123. data/lib/insika/followup_policy.rb +221 -0
  124. data/lib/insika/followup_store.rb +306 -0
  125. data/lib/insika/frontmatter.rb +1 -1
  126. data/lib/insika/funnel_declaration.rb +106 -0
  127. data/lib/insika/funnel_fold.rb +179 -0
  128. data/lib/insika/funnel_store.rb +163 -0
  129. data/lib/insika/golden_store.rb +3 -3
  130. data/lib/insika/grounding/matcher.rb +69 -0
  131. data/lib/insika/grounding.rb +44 -0
  132. data/lib/insika/harvest/conversion_gate.rb +159 -0
  133. data/lib/insika/harvest/criterion.rb +98 -0
  134. data/lib/insika/harvest/gate.rb +194 -0
  135. data/lib/insika/harvest/negative_list.rb +199 -0
  136. data/lib/insika/harvest.rb +241 -0
  137. data/lib/insika/harvest_engine.rb +193 -0
  138. data/lib/insika/harvest_store.rb +548 -0
  139. data/lib/insika/http_client.rb +3 -3
  140. data/lib/insika/inbound_log.rb +1 -1
  141. data/lib/insika/llm_configurator.rb +3 -3
  142. data/lib/insika/loop_detector.rb +143 -0
  143. data/lib/insika/mcp_http_client.rb +4 -4
  144. data/lib/insika/mcp_tool_ingestor.rb +6 -6
  145. data/lib/insika/media.rb +298 -0
  146. data/lib/insika/memory_audit_store.rb +85 -0
  147. data/lib/insika/memory_store.rb +264 -23
  148. data/lib/insika/message_origin.rb +8 -3
  149. data/lib/insika/model_resolver.rb +1 -1
  150. data/lib/insika/model_selection.rb +5 -4
  151. data/lib/insika/model_visible.rb +87 -0
  152. data/lib/insika/model_visible_trace_store.rb +66 -0
  153. data/lib/insika/onboarding.rb +8 -3
  154. data/lib/insika/outbox_store.rb +44 -6
  155. data/lib/insika/outcome_store.rb +147 -0
  156. data/lib/insika/overlay_tool_registry.rb +3 -4
  157. data/lib/insika/pack.rb +3 -3
  158. data/lib/insika/pack_importer.rb +17 -15
  159. data/lib/insika/packaging.rb +163 -0
  160. data/lib/insika/parity/criterion.rb +79 -0
  161. data/lib/insika/parity/verdict.rb +318 -0
  162. data/lib/insika/pending_action_store.rb +1 -1
  163. data/lib/insika/plugin/loader.rb +2 -2
  164. data/lib/insika/policy/policy.rb +1 -1
  165. data/lib/insika/prefix_fingerprint.rb +58 -0
  166. data/lib/insika/profile_source.rb +34 -7
  167. data/lib/insika/proposal_store.rb +271 -0
  168. data/lib/insika/provider_error_classifier.rb +160 -0
  169. data/lib/insika/queue_policy.rb +6 -3
  170. data/lib/insika/recovery.rb +47 -6
  171. data/lib/insika/refinement/candidate.rb +4 -4
  172. data/lib/insika/refinement/evidence_collector.rb +6 -6
  173. data/lib/insika/refinement/gate.rb +7 -7
  174. data/lib/insika/refinement/panel.rb +7 -7
  175. data/lib/insika/refinement/proposer.rb +10 -10
  176. data/lib/insika/refinement_store.rb +12 -12
  177. data/lib/insika/reliability.rb +211 -0
  178. data/lib/insika/retention.rb +281 -0
  179. data/lib/insika/routing.rb +101 -0
  180. data/lib/insika/safety/config.rb +46 -6
  181. data/lib/insika/safety/corpus.rb +255 -0
  182. data/lib/insika/safety/detectors.rb +34 -115
  183. data/lib/insika/safety/factory.rb +18 -5
  184. data/lib/insika/safety/grounding_enforcer.rb +59 -0
  185. data/lib/insika/safety/grounding_validator.rb +49 -0
  186. data/lib/insika/safety/input_guardrail.rb +20 -5
  187. data/lib/insika/safety/moderator.rb +19 -11
  188. data/lib/insika/safety/output_filter.rb +10 -6
  189. data/lib/insika/safety/output_validator.rb +13 -7
  190. data/lib/insika/safety/safe_responses.rb +1 -1
  191. data/lib/insika/sandbox/boundary.rb +2 -2
  192. data/lib/insika/sandbox.rb +1 -1
  193. data/lib/insika/schema_guard.rb +35 -0
  194. data/lib/insika/server/app.rb +366 -54
  195. data/lib/insika/server/boot.rb +4 -4
  196. data/lib/insika/server/rack_app.rb +31 -7
  197. data/lib/insika/server/responses.rb +58 -9
  198. data/lib/insika/server/tenant_auth.rb +61 -0
  199. data/lib/insika/session_actor.rb +11 -7
  200. data/lib/insika/session_store.rb +66 -3
  201. data/lib/insika/settings_store.rb +15 -5
  202. data/lib/insika/shadow_pair_store.rb +258 -0
  203. data/lib/insika/shutdown.rb +4 -4
  204. data/lib/insika/skill_catalog.rb +131 -20
  205. data/lib/insika/skill_store.rb +70 -22
  206. data/lib/insika/soak/envelope.rb +140 -0
  207. data/lib/insika/soak/report.rb +392 -0
  208. data/lib/insika/soak/runner.rb +554 -0
  209. data/lib/insika/steer_injector.rb +1 -1
  210. data/lib/insika/store.rb +11 -2
  211. data/lib/insika/stores/memory.rb +6 -0
  212. data/lib/insika/stores/sqlite.rb +8 -0
  213. data/lib/insika/studio/app.rb +1058 -75
  214. data/lib/insika/studio/assets/dist/application.css +1 -1
  215. data/lib/insika/studio/assets/dist/application.js +27 -26
  216. data/lib/insika/studio/assets/dist/favicon.svg +6 -0
  217. data/lib/insika/studio/forms.rb +274 -22
  218. data/lib/insika/studio/nav_icons.rb +7 -2
  219. data/lib/insika/studio/views/_message.erb +2 -2
  220. data/lib/insika/studio/views/agent_detail.erb +629 -86
  221. data/lib/insika/studio/views/agents.erb +11 -7
  222. data/lib/insika/studio/views/approvals.erb +4 -1
  223. data/lib/insika/studio/views/chats.erb +4 -1
  224. data/lib/insika/studio/views/customer.erb +94 -0
  225. data/lib/insika/studio/views/customers.erb +32 -0
  226. data/lib/insika/studio/views/evals.erb +4 -1
  227. data/lib/insika/studio/views/facts.erb +133 -0
  228. data/lib/insika/studio/views/followups.erb +125 -0
  229. data/lib/insika/studio/views/funnel.erb +106 -0
  230. data/lib/insika/studio/views/harvest.erb +234 -0
  231. data/lib/insika/studio/views/home.erb +2 -1
  232. data/lib/insika/studio/views/layout.erb +1 -0
  233. data/lib/insika/studio/views/parity.erb +147 -0
  234. data/lib/insika/studio/views/playground.erb +7 -1
  235. data/lib/insika/studio/views/refinement.erb +4 -4
  236. data/lib/insika/studio/views/session.erb +133 -3
  237. data/lib/insika/studio/views/settings.erb +9 -12
  238. data/lib/insika/studio/views/skills.erb +66 -12
  239. data/lib/insika/studio/views/system_files.erb +1 -1
  240. data/lib/insika/studio/views/task.erb +13 -0
  241. data/lib/insika/studio/views/tasks.erb +4 -1
  242. data/lib/insika/studio/views/tools.erb +0 -1
  243. data/lib/insika/subagent_graph.rb +3 -3
  244. data/lib/insika/task_actor.rb +3 -3
  245. data/lib/insika/task_store.rb +22 -2
  246. data/lib/insika/telemetry/pricing.rb +3 -3
  247. data/lib/insika/telemetry/recorder.rb +1 -1
  248. data/lib/insika/telemetry.rb +2 -2
  249. data/lib/insika/testing/store_contract.rb +54 -33
  250. data/lib/insika/tick.rb +146 -0
  251. data/lib/insika/token_store.rb +168 -0
  252. data/lib/insika/tool_assembly.rb +5 -5
  253. data/lib/insika/tool_definition.rb +25 -15
  254. data/lib/insika/tool_envelope.rb +70 -1
  255. data/lib/insika/tool_manifest.rb +11 -7
  256. data/lib/insika/tool_output_compressor.rb +100 -0
  257. data/lib/insika/tool_store.rb +1 -1
  258. data/lib/insika/tool_trace_store.rb +1 -1
  259. data/lib/insika/tools/concurrency.rb +2 -2
  260. data/lib/insika/tools/data_defined_tool.rb +14 -5
  261. data/lib/insika/tools/generate_image.rb +44 -0
  262. data/lib/insika/tools/load_skill.rb +61 -3
  263. data/lib/insika/tools/schedule_followup.rb +164 -0
  264. data/lib/insika/tools/stuck_signal.rb +44 -0
  265. data/lib/insika/tools/subagent.rb +4 -4
  266. data/lib/insika/tools/subagents.rb +1 -1
  267. data/lib/insika/tools/tts.rb +47 -0
  268. data/lib/insika/tools/update_briefing.rb +126 -0
  269. data/lib/insika/turn_output.rb +2 -2
  270. data/lib/insika/turn_state.rb +54 -13
  271. data/lib/insika/turn_timing.rb +24 -4
  272. data/lib/insika/usage_ledger.rb +1 -1
  273. data/lib/insika/version.rb +1 -1
  274. data/lib/insika/vitals.rb +84 -0
  275. data/lib/insika/wiring/graph.rb +372 -34
  276. data/lib/insika/workflow.rb +1 -1
  277. data/lib/insika/workflow_registry.rb +1 -1
  278. data/lib/insika.rb +122 -16
  279. metadata +95 -2
  280. data/lib/insika/server/admin_auth.rb +0 -29
@@ -26,16 +26,16 @@ module Insika
26
26
  # the runtime does not care how many agents it hosts, only that each one
27
27
  # arrives as an ordinary Pack.
28
28
  #
29
- # `backend`: the store this graph owns (RFC-0017 A1). nil = the historic
29
+ # `backend`: the store this graph owns. nil = the historic
30
30
  # path, `INSIKA_DB` or memory — ENV is a default, never a requirement (the
31
- # embed contract, item 5). An injected backend wins and is never widened
31
+ # embed contract). An injected backend wins and is never widened
32
32
  # back to the environment.
33
33
  def initialize(definition, backend: nil)
34
34
  @definition = definition
35
35
  @injected_backend = backend
36
36
  @packs = (definition.respond_to?(:packs) ? Array(definition.packs) : [definition.pack]).freeze
37
37
  @pack = @packs.first
38
- # RFC-0017 A2: the graph's OWN RubyLLM config, built before the graph so
38
+ # the graph's OWN RubyLLM config, built before the graph so
39
39
  # every collaborator that talks to a provider is handed it at wiring time
40
40
  # rather than reading a process-wide singleton at call time.
41
41
  @llm = build_llm_context
@@ -44,7 +44,7 @@ module Insika
44
44
  import_packs
45
45
  end
46
46
 
47
- # The graph's RubyLLM context (RFC-0017 A2) — an isolated config dup that
47
+ # The graph's RubyLLM context — an isolated config dup that
48
48
  # answers #chat. nil only under the test stub, where the fallback is the
49
49
  # RubyLLM constant itself. Read by the specs and by anything a host wires
50
50
  # alongside this graph.
@@ -178,7 +178,8 @@ module Insika
178
178
  guardrails: c[:guardrails], context_providers: context_providers(spine, c),
179
179
  edge_limiter: c[:edge_limiter],
180
180
  executor_extra: { settings_store: c[:settings_store], tool_trace_store: c[:tool_trace_store],
181
- llm: @llm }
181
+ context_trace_store: c[:context_trace_store],
182
+ cache_series_store: c[:cache_series_store], llm: @llm }
182
183
  )
183
184
  register_authoring_commands(graph, c)
184
185
  register_workflows(graph)
@@ -228,17 +229,21 @@ module Insika
228
229
  system_file_store: Insika::SystemFileStore.new(config_store: config_store),
229
230
  mcp_store: Insika::McpStore.new(config_store: config_store),
230
231
  tool_trace_store: Insika::ToolTraceStore.new(store: backend),
232
+ context_trace_store: Insika::ContextTraceStore.new(store: backend),
233
+ # the per-AGENT cache-hit series (the Studio agent-detail card).
234
+ cache_series_store: Insika::CacheSeriesStore.new(store: backend),
231
235
  tool_registry: tool_registry,
232
236
  tool_catalog: Insika::ToolCatalog.new(tool_registry: tool_registry),
233
237
  skill_catalog: Insika::SkillCatalog.new([], store: skill_store),
234
238
  prompt_catalog: Insika::PromptCatalog.new([]),
235
239
  profile_source: Insika::StoredProfileSource.new(config_store: config_store),
236
- # A2: the moderator/validator tiers ask a model too. A graph reading its
240
+ # the moderator/validator tiers ask a model too. A graph reading its
237
241
  # own key for the turn but the global one for a guardrail would be a
238
242
  # credential leak wearing the look of isolation.
239
243
  guardrails: Insika::Safety::Factory.new(settings_store: settings_store, llm: @llm),
240
244
  edge_limiter: Insika::EdgeLimiter.new(
241
- ledger: Insika::UsageLedger.new(store: backend), settings_store: settings_store
245
+ ledger: Insika::UsageLedger.new(store: backend), settings_store: settings_store,
246
+ budget_ledger: spine.budget_ledger, event_stream: spine.event_stream
242
247
  )
243
248
  }
244
249
  end
@@ -251,6 +256,7 @@ module Insika
251
256
  agent_files: c[:agent_file_store], system_files: c[:system_file_store]
252
257
  ),
253
258
  Insika::Context::Providers::Skill.new(catalog: c[:skill_catalog]),
259
+ Insika::Context::Providers::SkillTrigger.new(catalog: c[:skill_catalog]),
254
260
  Insika::Context::Providers::ToolSearch.new(catalog: c[:tool_catalog]),
255
261
  Insika::Context::Providers::Memory.new(store: spine.memory_store),
256
262
  Insika::Context::Providers::Session.new(session_store: spine.session_store)
@@ -264,6 +270,7 @@ module Insika
264
270
  bus = graph.bus
265
271
  es = graph.event_stream
266
272
  mem = graph.memory_store
273
+ audit = graph.memory_audit_store
267
274
  bus.register(:create_agent, Insika::Commands::CreateAgent.new(profile_source: c[:profile_source], event_stream: es))
268
275
  bus.register(:update_agent, Insika::Commands::UpdateAgent.new(profile_source: c[:profile_source], event_stream: es))
269
276
  bus.register(:delete_agent, Insika::Commands::DeleteAgent.new(profile_source: c[:profile_source], event_stream: es))
@@ -273,9 +280,11 @@ module Insika
273
280
  bus.register(:restore_agent_file, Insika::Commands::RestoreAgentFile.new(profile_source: c[:profile_source], agent_file_store: c[:agent_file_store], event_stream: es))
274
281
  bus.register(:write_skill, Insika::Commands::WriteSkill.new(skill_store: c[:skill_store], skill_catalog: c[:skill_catalog], event_stream: es))
275
282
  bus.register(:set_skill_agents, Insika::Commands::SetSkillAgents.new(profile_source: c[:profile_source], event_stream: es))
276
- bus.register(:memory_put_fact, Insika::Commands::MemoryPutFact.new(memory_store: mem, event_stream: es))
277
- bus.register(:memory_forget_fact, Insika::Commands::MemoryForgetFact.new(memory_store: mem, event_stream: es))
283
+ bus.register(:memory_put_fact, Insika::Commands::MemoryPutFact.new(memory_store: mem, event_stream: es, audit_store: audit))
284
+ bus.register(:memory_forget_fact, Insika::Commands::MemoryForgetFact.new(memory_store: mem, event_stream: es, audit_store: audit))
278
285
  bus.register(:memory_add_note, Insika::Commands::MemoryAddNote.new(memory_store: mem, event_stream: es))
286
+ # the LGPD access right — the Studio Customers drill exports.
287
+ bus.register(:export_customer_memory, Insika::Commands::ExportCustomerMemory.new(memory_store: mem, event_stream: es))
279
288
  bus.register(:update_settings, Insika::Commands::UpdateSettings.new(settings_store: c[:settings_store], event_stream: es))
280
289
  bus.register(:upsert_llm_provider, Insika::Commands::UpsertLLMProvider.new(provider_store: c[:provider_store], configurator: c[:configurator], event_stream: es))
281
290
  bus.register(:delete_llm_provider, Insika::Commands::DeleteLLMProvider.new(provider_store: c[:provider_store], configurator: c[:configurator], event_stream: es))
@@ -303,7 +312,7 @@ module Insika
303
312
  ([provider_name] + @packs.filter_map { |p| Insika::Coercion.presence(p.config[:provider])&.to_s }).uniq
304
313
  end
305
314
 
306
- # RFC-0017 A2 — the graph's credentials belong to the graph. `RubyLLM.context`
315
+ # the graph's credentials belong to the graph. `RubyLLM.context`
307
316
  # is an isolated dup of the config that answers #chat, so two graphs in one
308
317
  # process no longer overwrite each other's keys: the global
309
318
  # `RubyLLM.configure` is a single slot PER PROVIDER, and the loser of that
@@ -332,7 +341,7 @@ module Insika
332
341
 
333
342
  # The LLMConfigurator's target (Studio: edit a provider key, no restart).
334
343
  # Scoped to THIS graph's context — a `RubyLLM.configure` here would reach
335
- # into every other graph in the process, which is the leak A2 exists to
344
+ # into every other graph in the process, which is the leak exists to
336
345
  # close. nil = no context (test stub) -> the configurator's own global
337
346
  # default, unchanged.
338
347
  def llm_configure
@@ -25,23 +25,28 @@ module Insika
25
25
  # Fixed local token: logs into the Studio (cookie) and gates /v1 (Bearer).
26
26
  # Never a real secret — override with `token:` or ADMIN_TOKEN.
27
27
  @token = token || ENV.fetch("ADMIN_TOKEN", "local-demo")
28
- # RFC-0017 A3: the /v1 app is a value now — the same one a host mounts.
28
+ # the /v1 app is a value now — the same one a host mounts.
29
29
  # This boot adds the Studio and a reactor around it, nothing else.
30
30
  @builder = Insika::Server::AppBuilder.new(runtime, token: @token)
31
31
  end
32
32
 
33
33
  def run
34
+ # The boot refusal (shadow + no criterion) lives in the AppBuilder; run it
35
+ # BEFORE the Studio is configured so the criterion the Studio folds is the
36
+ # same object the delivery path stamps pairs with.
37
+ @builder.channels?
38
+ register_parity_commands
34
39
  configure_studio
35
40
  dispatch = Rack::URLMap.new("/studio" => Studio::App, "/" => @builder.app)
36
41
  endpoint = Async::HTTP::Endpoint.parse("http://#{@host}:#{@port}")
37
42
  middleware = Protocol::Rack::Adapter.new(dispatch)
38
- # Item 16 / P4: the OTEL bridge is opt-in but must be reachable from the DSL
43
+ # the OTEL bridge is opt-in but must be reachable from the DSL
39
44
  # front door too — the convention is worthless if only config.ru can export.
40
45
  # nil (the default) -> attach is a no-op and no gem is loaded.
41
46
  telemetry = Insika::Telemetry.setup(service_name: ENV.fetch("OTEL_SERVICE_NAME", "insika"))
42
47
 
43
48
  banner(telemetry)
44
- # RFC-0016 A3: first Ctrl-C/SIGTERM closes the intake and drains in-flight
49
+ # first Ctrl-C/SIGTERM closes the intake and drains in-flight
45
50
  # turns (INSIKA_DRAIN_TIMEOUT, default 20s); a second signal skips the wait.
46
51
  Insika::Shutdown.install(executor: @graph.executor)
47
52
  Async do
@@ -56,6 +61,23 @@ module Insika
56
61
  def workflows? = @builder.workflows?
57
62
  def channels? = @builder.channels?
58
63
 
64
+ # The parity page renders whenever a shadow channel exists, so the judge
65
+ # button must reach a registered command — the deployment bus registers the
66
+ # same one (config/deployment.rb); a page that answers "unknown command"
67
+ # would make this root a second-class citizen. Registered only when the
68
+ # criterion loaded (a command without one would be a fake judge).
69
+ def register_parity_commands
70
+ criterion = @builder.criterion
71
+ return unless criterion
72
+
73
+ @graph.bus.register(:judge_shadow_pairs,
74
+ Insika::Commands::JudgeShadowPairs.new(
75
+ shadow_pairs: @graph.shadow_pair_store,
76
+ settings_store: @rt.component(:settings_store),
77
+ criterion: criterion, event_stream: @graph.event_stream
78
+ ))
79
+ end
80
+
59
81
  def configure_studio
60
82
  Studio::App.configure(
61
83
  command_bus: @graph.bus, profile_source: @graph.profiles,
@@ -64,15 +86,63 @@ module Insika
64
86
  agent_file_store: @rt.component(:agent_file_store), skill_store: @rt.component(:skill_store),
65
87
  skill_catalog: @graph.skill_catalog, tool_catalog: @graph.tool_catalog,
66
88
  tool_store: @rt.component(:tool_store), memory_store: @graph.memory_store,
89
+ # the Customers drill renders the audit lines.
90
+ memory_audit_store: @graph.memory_audit_store,
67
91
  session_store: @graph.session_store,
68
92
  settings_store: @rt.component(:settings_store), llm_provider_store: @rt.component(:provider_store),
69
93
  mcp_store: @rt.component(:mcp_store), system_file_store: @rt.component(:system_file_store),
70
94
  tool_trace_store: @rt.component(:tool_trace_store),
95
+ context_trace_store: @rt.component(:context_trace_store),
96
+ # the per-agent cache-hit series (agent-detail card).
97
+ cache_series_store: @rt.component(:cache_series_store),
71
98
  task_store: @graph.task_store, checkpoint_store: @graph.checkpoint_store,
72
- pending_action_store: @graph.pending_action_store
99
+ pending_action_store: @graph.pending_action_store,
100
+ # WS7: the agents-grid scorecard reads the outcome store.
101
+ outcome_store: @graph.outcome_store,
102
+ # the outcome-funnel page reads the fold's cells and the
103
+ # BudgetLedger's current counters (D6); the only mutation — the
104
+ # baseline freeze — dispatches :freeze_funnel_baseline on the bus.
105
+ funnel_store: @graph.funnel_store,
106
+ budget_ledger: @graph.budget_ledger,
107
+ # the parity page (only rendered when a shadow channel exists —
108
+ # the nav row keys off the registry).
109
+ shadow_pair_store: @graph.shadow_pair_store,
110
+ parity_criterion: @builder.criterion,
111
+ channel_registry: @graph.channel_registry,
112
+ # the Follow-ups page reads the stores directly;
113
+ # its only mutations (cancel, force-revoke) dispatch bus commands.
114
+ followup_store: @graph.followup_store,
115
+ contact_store: @graph.contact_store,
116
+ # the Facts (wiki) page reads the proposal store
117
+ # directly; its mutations dispatch :resolve_proposal on the bus.
118
+ proposal_store: @graph.proposal_store,
119
+ # the Harvest page reads the harvest store + the two
120
+ # pre-registered artifacts directly; its mutations dispatch the
121
+ # harvest bus commands. The criterion/negative list load best-effort
122
+ # (nil = the page renders the refusal/empty states).
123
+ harvest_store: @graph.harvest_store,
124
+ harvest_criterion: harvest_criterion,
125
+ negative_list: harvest_negative_list
73
126
  )
74
127
  end
75
128
 
129
+ # The frozen criterion — best-effort at boot: a bare install
130
+ # without the env var renders the ruler's hole instead of
131
+ # crashing the Studio.
132
+ def harvest_criterion
133
+ path = Insika::EnvSchema.read("INSIKA_HARVEST_CRITERION")
134
+ path && Insika::Harvest::Criterion.load(path)
135
+ rescue Insika::ConfigError, Insika::ValidationError
136
+ nil
137
+ end
138
+
139
+ def harvest_negative_list
140
+ path = Insika::EnvSchema.read("INSIKA_HARVEST_NEGATIVE")
141
+ path && Insika::Harvest::NegativeList.parse(File.read(path))
142
+ rescue Errno::ENOENT
143
+ nil
144
+ end
145
+
76
146
  def persistence
77
147
  @graph.durable? ? "durable (sqlite)" : "ephemeral (memory)"
78
148
  end
@@ -17,7 +17,7 @@ module Insika
17
17
  class System
18
18
  attr_reader :definitions, :workflows, :runtime_options
19
19
 
20
- # `backend`: the store this system's graph owns (RFC-0017 A1). nil = the
20
+ # `backend`: the store this system's graph owns. nil = the
21
21
  # historic path (INSIKA_DB, or memory when unset). Set by `Insika.embed`.
22
22
  def initialize(definitions:, workflows: [], runtime: {}, backend: nil)
23
23
  @definitions = definitions.freeze
data/lib/insika/dsl.rb CHANGED
@@ -3,17 +3,17 @@
3
3
  require_relative "pack"
4
4
 
5
5
  module Insika
6
- # Public Ruby DSL (item 36 / §13.3) — the OSS "business card":
6
+ # Public Ruby DSL — the OSS "business card":
7
7
  #
8
8
  # agent = Insika.agent("assistant") do
9
- # model "deepseek-chat"
9
+ # model "deepseek-v4-flash"
10
10
  # instructions "You are a concise, friendly assistant."
11
11
  # end
12
12
  # puts agent.reply("hi, what can you do?") # one turn, in-process
13
13
  # agent.serve # control UI + /v1 on :9292
14
14
  #
15
15
  # It is THIN SUGAR that GENERATES the data (a Insika::Pack), never a bypass of
16
- # config-over-code (COMPETITIVE-ANALYSIS §4.1). `Insika.agent { … }.to_pack`
16
+ # config-over-code (COMPETITIVE-ANALYSIS). `Insika.agent { … }.to_pack`
17
17
  # is the same portable artifact the PackImporter consumes at runtime — the DSL
18
18
  # and a hand-written pack produce the SAME profile (the parity spec proves it),
19
19
  # because BOTH go through the standard import → StoredProfileSource round-trip.
@@ -153,7 +153,7 @@ module Insika
153
153
 
154
154
  # A DATA-DEFINED (declarative HTTP) tool — pure config-over-code. `defn` is a
155
155
  # ToolDefinition hash (name/description/parameters/binding…). Its name is
156
- # auto-added to the allowlist so the agent can call its own tool (NF2).
156
+ # auto-added to the allowlist so the agent can call its own tool.
157
157
  def data_tool(defn)
158
158
  h = defn.transform_keys(&:to_s)
159
159
  @tools << h
@@ -173,9 +173,31 @@ module Insika
173
173
  n
174
174
  end
175
175
 
176
+ # skills_eager — turns progressive disclosure off for THIS agent, wholly or in
177
+ # part. The body of an eager skill is in the prompt on every turn, so its
178
+ # activation is not a decision and cannot be missed; it is paid for on every
179
+ # turn, so measure the bodies against `context_budget` first (a stable position
180
+ # makes them a cacheable prefix).
181
+ #
182
+ # skills_eager # every allowed skill
183
+ # skills_eager "formato", "markers" # exactly these
184
+ # skills_eager false # none (the default)
185
+ #
186
+ # A LIST and not a per-skill flag because skills are shared: `escalation-to-human`
187
+ # sits in several allowlists, and one flag on the skill would force one decision
188
+ # onto every agent holding it.
189
+ def skills_eager(*names)
190
+ flat = names.flatten
191
+ @config[:skills_eager] =
192
+ if flat.empty? then true
193
+ elsif flat == [true] || flat == [false] then flat.first
194
+ else flat.map(&:to_s)
195
+ end
196
+ end
197
+
176
198
  # --- delegation ------------------------------------------------------
177
199
  # subagents "security", "performance" → the child agents this one MAY
178
- # spawn (RFC-0010). CAPACITY field: opt-in, never inherited, and the ids
200
+ # spawn. CAPACITY field: opt-in, never inherited, and the ids
179
201
  # must be agents of the same system (`Insika.system { … }`) or already in
180
202
  # the store. Present ⇒ the engine wires `spawn_subagent`/`spawn_subagents`.
181
203
  def subagents(*ids)
@@ -185,23 +207,138 @@ module Insika
185
207
  # --- knobs -----------------------------------------------------------
186
208
  def memory(on = true) = @config[:memory] = on
187
209
 
188
- # Content-safety guardrails (RFC-0009) — opt-in and configurable per agent.
210
+ # Spend caps per calendar window (WS2): daily/monthly token budgets for
211
+ # this agent, per (tenant, agent) when multi-tenant. HARD is the default:
212
+ # absent `soft:` (or `soft: false`) turns the cap into a hard wall (the
213
+ # turn fails with budget_exceeded + retry_after); `soft: true` warns once
214
+ # per window and keeps running.
215
+ # budget daily: 100_000, monthly: 2_000_000, soft: false
216
+ def budget(hash) = (@config[:budget] ||= {}).merge!(hash.transform_keys(&:to_s))
217
+
218
+ # The outcome funnel declaration: the store's stage vocabulary
219
+ # as pack data — the engine folds WS7 outcome kinds into the DECLARED
220
+ # stages, and never hard-codes one itself. Merges, so repeated calls
221
+ # accumulate (like budget).
222
+ # funnel stages: %w[greeted qualified cart paid],
223
+ # advance_on: { "pix_paid" => "paid", "abandoned_cart" => "cart" },
224
+ # primary: "paid", attribution_window: "72h"
225
+ def funnel(hash) = (@config[:funnel] ||= {}).merge!(hash.transform_keys(&:to_s))
226
+
227
+ # The follow-up declaration: the agent may book a follow-up
228
+ # with a customer at a future time (`schedule` tool); the engine fires the
229
+ # synthetic turn and enforces the policy (quiet hours, max frequency,
230
+ # cancellation keywords, silence detection) at fire time. Pack data —
231
+ # merges, so repeated calls accumulate (like budget).
232
+ # followup arm: "schedule",
233
+ # policy: { quiet_hours: { timezone: "America/Sao_Paulo",
234
+ # start: "21:30", end: "09:00" },
235
+ # max_frequency: "2/24h",
236
+ # cancel_keywords: ["não quero mais contato"],
237
+ # silence_after_sends: 3 }
238
+ def followup(hash) = (@config[:followup] ||= {}).merge!(hash.transform_keys(&:to_s))
239
+
240
+ # The session-distillation declaration: what counts as a fact
241
+ # for THIS store, distilled from finished customer conversations. Pack
242
+ # data — merges, so repeated calls accumulate (like budget). `prompt` and
243
+ # `model` are pack-authored keys the DSL passes through.
244
+ # distill enabled: true, idle_hours: 6, max_proposals: 10
245
+ def distill(hash) = (@config[:distill] ||= {}).merge!(hash.transform_keys(&:to_s))
246
+
247
+ # The gated-harvest declaration: the engine may read this
248
+ # agent's finished traffic and propose SKILLS for the store's playbook —
249
+ # through the negative list, the grounding filter and the double gate,
250
+ # never applied automatically. Pack data — merges, so repeated calls
251
+ # accumulate (like budget). `prompt`/`model` are pack-authored keys the
252
+ # DSL passes through.
253
+ # harvest enabled: true,
254
+ # negative_list: [ { rule: "no-competitor-prices", pattern: "concorrente" } ],
255
+ # miner: { model: "deepseek-v4-flash", window: { last_sessions: 200 } }
256
+ def harvest(hash) = (@config[:harvest] ||= {}).merge!(hash.transform_keys(&:to_s))
257
+
258
+ # Provider-interaction reliability, as DATA (WS3): retries + exponential
259
+ # backoff on transient failures, a fallback model chain (mid-turn
260
+ # rotation), and a circuit breaker per (tenant, provider/model) that
261
+ # fail-fasts once the window trips. `fallback`/`circuit_breaker` entries
262
+ # are "provider/model" refs or plain model ids.
263
+ # reliability retries: 3, backoff: "exponential",
264
+ # fallback: ["gpt-4o-mini"], circuit_breaker: { after: 10, within: 60, cooldown: 300 }
265
+ def reliability(hash)
266
+ (@config[:reliability] ||= {}).merge!(hash.transform_keys(&:to_s))
267
+ end
268
+
269
+ # Operator alert delivery (WS6): POST this agent's budget_warning /
270
+ # breaker_open / delivery_failed events to the webhook as JSON.
271
+ def alerts(hash) = (@config[:alerts] ||= {}).merge!(hash.transform_keys(&:to_s))
272
+
273
+ # Intent routing (WS4): classify each turn's message into one route with a
274
+ # cheap model BEFORE the ask. A Hash: route name -> description (or a Hash
275
+ # with description/delegate/stuck/message), plus the reserved keys
276
+ # "default" (the deterministic fallback) and "model"/"provider" (the cheap
277
+ # classifier). The classifier prompt is generated — data only.
278
+ # routes "shopping" => "the customer wants to browse products",
279
+ # "order" => { "description" => "asks about an existing order",
280
+ # "delegate" => "order-agent" },
281
+ # "human" => { "description" => "the customer asks for a person",
282
+ # "stuck" => true },
283
+ # "default" => "shopping", "model" => "deepseek-v4-flash"
284
+ def routes(hash) = (@config[:routes] ||= {}).merge!(hash.transform_keys(&:to_s))
285
+
286
+ # The agent may signal it cannot proceed (WS5): when on, the model
287
+ # can call `signal_stuck`, which ends the turn with `outcome: :stuck` + a final
288
+ # message + a `:turn_stuck` event. What "stuck" means is the consumer's call.
289
+ # stuck_signal true
290
+ def stuck_signal(on = true) = @config[:stuck_signal] = on
291
+
292
+ # The per-session working-state schema this agent keeps and asks for
293
+ # a flat list of field names. [] = off (no provider output,
294
+ # no update_briefing/set_next_step tools).
295
+ # briefing_fields "size", "budget", "delivery_day"
296
+ def briefing_fields(*names)
297
+ @config[:briefing_fields] = names.flatten.map(&:to_s)
298
+ end
299
+
300
+ # Generated-media output policy (WS9, saída): the media kinds this
301
+ # agent MAY generate as turn outputs, with per-kind config. The other
302
+ # half of the gate is the CHANNEL's: the request must declare the
303
+ # matching capability for the tools to exist at all.
304
+ # outputs image: { model: "gpt-image-1", size: "1024x1024" },
305
+ # tts: { model: "tts-1", voice: "alloy" }
306
+ def outputs(hash) = (@config[:outputs] ||= {}).merge!(hash.transform_keys(&:to_s))
307
+
308
+ # The engine's "Tool discipline" block in the system prompt (retry a
309
+ # weak/empty tool result with a different approach before giving up).
310
+ # ON by default — this setter exists to turn it OFF:
311
+ # tool_persistence false
312
+ def tool_persistence(on = true) = @config[:tool_persistence] = on
313
+
314
+ # Mechanical tool-result dedupe in the replayed history
315
+ # (no-LLM compaction, apt for bloated transcripts). CHANGES WHAT THE MODEL
316
+ # SEES: repeated identical tool results collapse to a back-reference.
317
+ def tool_output_compression(on = true) = @config[:tool_output_compression] = on
318
+
319
+ # Content-safety guardrails — opt-in and configurable per agent.
189
320
  # Pure config-over-code: the hash is stored on the profile and consumed by
190
321
  # Safety::Config.from_profile. Merges, so repeated calls accumulate.
191
322
  # guardrails input: true, output: true, strictness: "medium",
192
- # moderator: "deepseek/deepseek-chat",
323
+ # moderator: "deepseek/deepseek-v4-flash",
193
324
  # responses: { "injection" => "I can't help with that." }
194
325
  def guardrails(hash) = (@config[:guardrails] ||= {}).merge!(hash.transform_keys(&:to_s))
195
326
 
196
- # Refinement (RFC-0013) — how the agent's own instruction files may be
327
+ # Evidence-grounding policy: the pack declares how the engine
328
+ # polices product claims against the evidence ledger. Same
329
+ # config-over-code shape as `guardrails`; absent = off (parity).
330
+ # grounding mode: :flag, matcher: { sku: '\b[A-Z]{2,4}\d{4,8}\b' }
331
+ def grounding(hash = {}) = (@config[:grounding] ||= {}).merge!(hash.transform_keys(&:to_s))
332
+
333
+ # Refinement — how the agent's own instruction files may be
197
334
  # improved from real traffic. Same config-over-code shape as `guardrails`;
198
- # omitting it entirely leaves the agent report-only (phase A writes nothing).
335
+ # omitting it entirely leaves the agent report-only (writes nothing).
199
336
  # refine mode: "propose", window: { last_sessions: 200 }, files: %w[TOOLS.md],
200
- # proposers: ["deepseek/deepseek-chat", "gpt-5-mini"],
337
+ # proposers: ["deepseek/deepseek-v4-flash", "gpt-5-mini"],
201
338
  # budget: { tokens: 200_000 }
202
339
  def refine(hash) = (@config[:refinement] ||= {}).merge!(hash.transform_keys(&:to_s))
203
340
 
204
- # Facts about THIS deployment that are not tools (RFC-0014 §3.5), so an eval
341
+ # Facts about THIS deployment that are not tools, so an eval
205
342
  # case can declare what it needs and be skipped where it is absent instead of
206
343
  # failing for the wrong reason.
207
344
  # declares "promotions", "human_handoff"
@@ -217,7 +354,7 @@ module Insika
217
354
  # edge_stream thinking: true, intermediate: false
218
355
  def edge_stream(hash) = (@config[:edge_stream] ||= {}).merge!(hash.transform_keys(&:to_s))
219
356
 
220
- # LLM generation params (v2, §10). `param :temperature, 0.2` or `params(...)`.
357
+ # LLM generation params. `param:temperature, 0.2` or `params(...)`.
221
358
  def param(key, value) = (@config[:params] ||= {})[key.to_sym] = value
222
359
  def params(hash) = (@config[:params] ||= {}).merge!(hash.transform_keys(&:to_sym))
223
360
  def temperature(value) = param(:temperature, value)
@@ -281,14 +418,14 @@ module Insika
281
418
  DSL.system(&block)
282
419
  end
283
420
 
284
- # RFC-0017 A1 — the front door for MOUNTING Insika into an app you already
421
+ # the front door for MOUNTING Insika into an app you already
285
422
  # have. Same block as `Insika.system`, one added obligation: the caller names
286
423
  # the store, so the graph stops discovering it from `INSIKA_DB` and two graphs
287
424
  # in one process can no longer read each other's sessions.
288
425
  #
289
426
  # INSIKA = Insika.embed(backend: Insika::Stores::SQLite.new(path: "storage/insika.sqlite3")) do
290
427
  # agent "support" do
291
- # model "deepseek-chat"
428
+ # model "deepseek-v4-flash"
292
429
  # instructions "…"
293
430
  # end
294
431
  # end
@@ -296,7 +433,7 @@ module Insika
296
433
  # mount Insika::Server.rack_app(INSIKA, token: ENV.fetch("INSIKA_TOKEN")), at: "/ai"
297
434
  #
298
435
  # It is a thin front door over the SAME assembly `Insika.system` uses — there is
299
- # one pipeline (RFC-0001), and the parity spec holds it to that. What an embedded
436
+ # one pipeline, and the parity spec holds it to that. What an embedded
300
437
  # graph owns, and what it still shares with the process, is docs/EMBEDDING.md.
301
438
  def embed(backend:, &block)
302
439
  DSL.embed(backend: backend, &block)