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
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Insika
4
4
  module Wiring
5
- # SHARED composition core for both roots (§12 G4 / §11.2 B4): the minimal wiring
5
+ # SHARED composition core for both roots: the minimal wiring
6
6
  # (config/wiring.rb) and the concrete deployment (config/deployment.rb) were two
7
7
  # near-identical composition roots. The duplication — backend→stores, event
8
8
  # stream, registries + policy builtins, capability registry, hooks, the Executor,
@@ -28,7 +28,14 @@ module Insika
28
28
  db && !db.empty? ? Insika::Stores::SQLite.new(path: db) : Insika::Stores::Memory.new
29
29
  end
30
30
 
31
- # Phase 1 — the infra spine that is IDENTICAL across roots. `extra_policy_
31
+ # an integer tick knob from the env, nil when unset (the caller's
32
+ # default wins). Dual-read honors the deprecated HARNESS_* alias.
33
+ def tick_env(name, env = ENV)
34
+ value = Insika::EnvSchema.read(name, env)
35
+ value&.to_i
36
+ end
37
+
38
+ # the infra spine that is IDENTICAL across roots. `extra_policy_
32
39
  # builtins` covers the one real divergence (the minimal wiring also registers
33
40
  # :workflow_allowlist; the deployment does not expose workflows).
34
41
  def spine(backend:, extra_policy_builtins: {})
@@ -38,17 +45,54 @@ module Insika
38
45
  pending_action_store = Insika::PendingActionStore.new(store: backend)
39
46
  delegation_store = Insika::DelegationStore.new(store: backend)
40
47
  memory_store = Insika::MemoryStore.new(store: backend)
41
- # RFC-0011 §6.4/§6.5: the two durable halves of a Shape B channel — the
48
+ # the append-only, content-free audit of OPERATOR memory
49
+ # mutations. Built unconditionally (empty and free when nothing writes)
50
+ # so a deployment that turns it on later finds its audit already durable.
51
+ memory_audit_store = Insika::MemoryAuditStore.new(store: backend)
52
+ token_store = Insika::TokenStore.new(store: backend)
53
+ budget_ledger = Insika::BudgetLedger.new(store: backend)
54
+ circuit_state = Insika::CircuitState.new(store: backend)
55
+ # the two durable halves of a Shape B channel — the
42
56
  # replies still owed to a platform, and the retry window that stops a
43
57
  # redelivered webhook from becoming a second turn. Built unconditionally
44
58
  # (they are empty and free when no channel is registered) so a deployment
45
59
  # that turns a channel on later finds its state already durable.
46
60
  outbox_store = Insika::OutboxStore.new(store: backend)
61
+ # the shadow pair store — one durable record per mirrored
62
+ # exchange, written by our half and the incumbent's. Built unconditionally
63
+ # (empty and free when shadow is off) so turning it on later finds the
64
+ # state already durable, beside the outbox on the same runtime backend.
65
+ shadow_pair_store = Insika::ShadowPairStore.new(store: backend)
47
66
  inbound_log = Insika::InboundLog.new(store: backend)
48
- # RFC-0013 phase A: refinement RUNS (reports over real traffic). Runtime data,
67
+ # WS7: business outcomes per conversation, recorded by the operator or
68
+ # the integration (POST /v1/outcomes). Built unconditionally (empty and
69
+ # free when nothing records) so the Studio's scorecard always has a store.
70
+ outcome_store = Insika::OutcomeStore.new(store: backend)
71
+ # the funnel's durable aggregates — per-day stage counts,
72
+ # fold cursors and baseline snapshots. Built unconditionally (empty and
73
+ # free when no pack declares a funnel) so the fold, the doctor and the
74
+ # Studio always have a store to read/write.
75
+ funnel_store = Insika::FunnelStore.new(store: backend)
76
+ # the contact-state cells and the follow-up schedule
77
+ # records. Built unconditionally (empty and free when no pack declares
78
+ # followup) so the engine, the tools and the Studio always have stores.
79
+ contact_store = Insika::ContactStore.new(store: backend)
80
+ followup_store = Insika::FollowupStore.new(store: backend)
81
+ # the distilled proposals + the latched dedup ledger +
82
+ # the per-session markers. Built unconditionally (empty and free when
83
+ # no pack declares distill) so the wiki, the engine and the LGPD
84
+ # purges always have a store.
85
+ proposal_store = Insika::ProposalStore.new(store: backend)
86
+ # refinement RUNS (reports over real traffic). Runtime data,
49
87
  # same backend as sessions/tasks — the collector and the command that write it
50
88
  # are the root's business (deployment-only, like the memory commands).
51
89
  refinement_store = Insika::RefinementStore.new(store: backend)
90
+ # the harvest's durable half — mining runs, the per-
91
+ # candidate lifecycle, the append-only promotion log, the snapshots
92
+ # and the per-session markers. Built unconditionally (empty and free
93
+ # when no pack declares harvest) so the engine, the Studio and the
94
+ # LGPD purges always have a store.
95
+ harvest_store = Insika::HarvestStore.new(store: backend)
52
96
 
53
97
  code_tool_registry = Insika::ToolRegistry.new
54
98
  workflow_registry = Insika::WorkflowRegistry.new
@@ -64,8 +108,17 @@ module Insika
64
108
  session_store: session_store, task_store: task_store,
65
109
  checkpoint_store: checkpoint_store, pending_action_store: pending_action_store,
66
110
  delegation_store: delegation_store,
67
- memory_store: memory_store, refinement_store: refinement_store,
68
- outbox_store: outbox_store, inbound_log: inbound_log,
111
+ memory_store: memory_store, memory_audit_store: memory_audit_store,
112
+ refinement_store: refinement_store,
113
+ harvest_store: harvest_store,
114
+ token_store: token_store, budget_ledger: budget_ledger, circuit_state: circuit_state,
115
+ outbox_store: outbox_store, shadow_pair_store: shadow_pair_store,
116
+ inbound_log: inbound_log,
117
+ outcome_store: outcome_store,
118
+ funnel_store: funnel_store,
119
+ contact_store: contact_store,
120
+ followup_store: followup_store,
121
+ proposal_store: proposal_store,
69
122
  code_tool_registry: code_tool_registry,
70
123
  workflow_registry: workflow_registry, policy_registry: policy_registry,
71
124
  capability_registry: Insika::CapabilityRegistry.new, hooks: Insika::Hooks.new,
@@ -73,16 +126,16 @@ module Insika
73
126
  )
74
127
  end
75
128
 
76
- # Phase 2 — assemble the graph on top of a spine.
129
+ # assemble the graph on top of a spine.
77
130
  #
78
131
  # tool_registry: effective registry the Executor uses (plain REGISTRY at the
79
132
  # base; OverlayToolRegistry in the deployment).
80
133
  # guardrails: a Safety::Factory — its input_guardrail becomes the single
81
134
  # middleware and its output_validator the after-task hook, so
82
- # both wirings compose RFC-0009 identically.
135
+ # both wirings compose identically.
83
136
  # executor_extra: optional Executor kwargs a root adds (deployment passes
84
137
  # settings_store + tool_trace_store; the base passes none).
85
- # edge_limiter: optional EdgeLimiter (item 33 / §12 G7). It goes BEFORE
138
+ # edge_limiter: optional EdgeLimiter. It goes BEFORE
86
139
  # the InputGuardrail so a flood can't spend the LLM
87
140
  # moderator; nil = no edge (parity).
88
141
  def build(spine:, profiles:, tool_registry:, tool_catalog:, skill_catalog:,
@@ -98,39 +151,171 @@ module Insika
98
151
  policy_registry: spine.policy_registry, event_stream: spine.event_stream
99
152
  )
100
153
 
101
- # RFC-0011 §6.5. Always built: the registry starts empty, so `record` finds no
154
+ # Always built: the registry starts empty, so `record` finds no
102
155
  # channel and returns nil on every turn — the cost of wiring it is one nil
103
156
  # check per completed turn, and the alternative is a second wiring path that
104
157
  # only production exercises.
105
158
  channel_delivery = Insika::ChannelDelivery.new(
106
159
  channels: spine.channel_registry, outbox: spine.outbox_store,
107
- session_store: spine.session_store, event_stream: spine.event_stream
160
+ session_store: spine.session_store, event_stream: spine.event_stream,
161
+ shadow_pairs: spine.shadow_pair_store
108
162
  )
109
163
 
110
- executor = Insika::Executor.new(
111
- context_builder: context_builder, policy_engine: policy_engine,
112
- middleware: middleware, hooks: spine.hooks,
113
- tool_registry: tool_registry, skill_catalog: skill_catalog, profiles: profiles,
114
- session_store: spine.session_store, task_store: spine.task_store,
115
- checkpoint_store: spine.checkpoint_store, event_stream: spine.event_stream,
116
- workflow_registry: spine.workflow_registry, pending_action_store: spine.pending_action_store,
117
- capability_registry: spine.capability_registry, tool_catalog: tool_catalog,
118
- memory_store: spine.memory_store,
119
- content_filter_factory: guardrails.content_filter_factory, # RFC-0009: stream redaction
120
- delegation_store: spine.delegation_store, # RFC-0010 Fase 2: async delegation durability
121
- channel_delivery: channel_delivery, # RFC-0011 §6.5: out-of-band reply delivery
122
- **executor_extra
164
+ # WS3 provider reliability (retries/backoff/fallback/breaker) is ALWAYS wired —
165
+ # the profile's `reliability` data gates it, so the bare wiring is unchanged.
166
+ reliability = Insika::Reliability.new(circuit_store: spine.circuit_state,
167
+ event_stream: spine.event_stream)
168
+ executor = Insika::Executor.new(
169
+ context_builder: context_builder, policy_engine: policy_engine,
170
+ middleware: middleware, hooks: spine.hooks,
171
+ tool_registry: tool_registry, skill_catalog: skill_catalog, profiles: profiles,
172
+ session_store: spine.session_store, task_store: spine.task_store,
173
+ checkpoint_store: spine.checkpoint_store, event_stream: spine.event_stream,
174
+ workflow_registry: spine.workflow_registry, pending_action_store: spine.pending_action_store,
175
+ capability_registry: spine.capability_registry, tool_catalog: tool_catalog,
176
+ memory_store: spine.memory_store,
177
+ content_filter_factory: guardrails.content_filter_factory, # stream redaction
178
+ delegation_store: spine.delegation_store, # async delegation durability
179
+ channel_delivery: channel_delivery, # out-of-band reply delivery
180
+ reliability: reliability, # WS3: retries/fallback/breaker (data-gated)
181
+ grounding_enforcer: guardrails.grounding_enforcer, # :enforce cut
182
+ # the follow-up stores — the ChatBuilder wires the
183
+ # schedule/cancel_followup tools on them (parity when no pack declares).
184
+ contact_store: spine.contact_store,
185
+ followup_store: spine.followup_store,
186
+ # the model-visible trace rides executor_extra like the
187
+ # context trace — a module graph that omits it still builds (nil =
188
+ # parity). The base graph wires it over the same backend the context
189
+ # trace uses.
190
+ model_visible_trace_store: Insika::ModelVisibleTraceStore.new(store: spine.backend),
191
+ **executor_extra
192
+ )
193
+
194
+ # the ONE path that writes proposals, shared by the
195
+ # automatic DistillEngine loop and the bus command behind the Studio's
196
+ # "Run distillation now" button (one code path for both).
197
+ run_distillation = Insika::Commands::RunDistillation.new(
198
+ profiles: profiles, proposal_store: spine.proposal_store,
199
+ session_store: spine.session_store, memory_store: spine.memory_store,
200
+ settings_store: executor_extra[:settings_store],
201
+ event_stream: spine.event_stream
123
202
  )
124
203
 
125
- bus = build_core_bus(spine: spine, profiles: profiles, executor: executor)
204
+ bus = build_core_bus(spine: spine, profiles: profiles, executor: executor,
205
+ executor_extra: executor_extra, skill_catalog: skill_catalog,
206
+ run_distillation: run_distillation)
207
+
208
+ # the periodic tick (outbox drain + stale recovery sweep). Built
209
+ # here, after the bus, because its recovery half dispatches resume_task
210
+ # through it; handed to the Executor, which starts it as a child of the
211
+ # turn supervisor in serving mode. `INSIKA_TICK_INTERVAL=0` disables.
212
+ # WS8 retention rides the tick: always built, the settings knob
213
+ # (`retention_days`) gates it — the base graph (no settings_store)
214
+ # reads as OFF.
215
+ executor.tick = Insika::Tick.new(
216
+ store: spine.backend, channel_delivery: channel_delivery,
217
+ recovery: Insika::Recovery.new(
218
+ task_store: spine.task_store, checkpoint_store: spine.checkpoint_store, command_bus: bus
219
+ ),
220
+ retention: Insika::Retention.new(
221
+ store: spine.backend, session_store: spine.session_store,
222
+ task_store: spine.task_store, checkpoint_store: spine.checkpoint_store,
223
+ memory_store: spine.memory_store, outcome_store: spine.outcome_store,
224
+ tool_trace_store: executor_extra[:tool_trace_store],
225
+ context_trace_store: executor_extra[:context_trace_store],
226
+ # the model-visible traces die with their checkpoints.
227
+ model_visible_trace_store: Insika::ModelVisibleTraceStore.new(store: spine.backend),
228
+ outbox_store: spine.outbox_store,
229
+ shadow_pair_store: spine.shadow_pair_store,
230
+ settings_store: executor_extra[:settings_store],
231
+ budget_ledger: spine.budget_ledger, # WS2 counter GC (retention-independent)
232
+ funnel_store: spine.funnel_store, # fold dies with its source
233
+ followup_store: spine.followup_store, # records age out too
234
+ contact_store: spine.contact_store, # cells age out too
235
+ proposal_store: spine.proposal_store, # proposals age out too
236
+ harvest_store: spine.harvest_store # candidates/log/snapshots too
237
+ ),
238
+ interval: tick_env("INSIKA_TICK_INTERVAL") || Insika::Tick::DEFAULT_INTERVAL,
239
+ stale_after: tick_env("INSIKA_TICK_STALE_AFTER") || Insika::Tick::DEFAULT_STALE_AFTER
240
+ )
241
+ # the tick-driven outcome fold — wired here, after the
242
+ # Tick, because it reads the outcome/funnel stores of the spine and the
243
+ # profiles; the per-pair declaration gates it (nil funnel everywhere =
244
+ # an inert fold). Always built: a pack that declares a funnel later
245
+ # finds its fold already on the tick.
246
+ executor.tick.funnel = Insika::FunnelFold.new(
247
+ outcome_store: spine.outcome_store, funnel_store: spine.funnel_store,
248
+ profiles: profiles, store: spine.backend
249
+ )
250
+ # the tick-driven follow-up firer — the tick's third
251
+ # duty, wired here after the Tick, gated by its own claim window.
252
+ # Always built: a pack that declares followup later finds its firer
253
+ # already on the tick (inert when no profile declares followup).
254
+ executor.tick.followup = Insika::FollowupEngine.new(
255
+ store: spine.backend, followup_store: spine.followup_store,
256
+ contact_store: spine.contact_store, task_store: spine.task_store,
257
+ profiles: profiles, executor: executor, event_stream: spine.event_stream
258
+ )
259
+ # the distillation engine — the tick-duty that finds idle
260
+ # customer sessions and distills them on its own worker fiber (a
261
+ # supervisor child, started in serving mode next to the tick).
262
+ # Always built: a pack that declares `distill:` later finds its engine
263
+ # already wired — and INERT until one does (the engine gates its start
264
+ # and its passes on a declaring profile).
265
+ executor.distill_engine = Insika::DistillEngine.new(
266
+ store: spine.backend, proposal_store: spine.proposal_store,
267
+ session_store: spine.session_store,
268
+ runner: run_distillation,
269
+ profiles: profiles,
270
+ window: Insika::DistillEngine::DEFAULT_WINDOW
271
+ )
272
+ # the harvest engine — the tick-duty that finds idle,
273
+ # unmined sessions whose agent declares `harvest.enabled` and mines
274
+ # them on its own worker fiber (a supervisor child, started in serving
275
+ # mode next to the tick). Always built: INERT until a profile declares
276
+ # harvest (the engine gates its start and its passes on data). The
277
+ # runner is the same RunHarvest the bus serves, so the manual CLI and
278
+ # the automated loop share one code path.
279
+ run_harvest = Insika::Commands::RunHarvest.new(
280
+ profiles: profiles, harvest_store: spine.harvest_store,
281
+ session_store: spine.session_store, task_store: spine.task_store,
282
+ skill_store: skill_catalog.store, # the harvest's dedup reads the authored skills
283
+ tool_trace_store: executor_extra[:tool_trace_store],
284
+ settings_store: executor_extra[:settings_store],
285
+ negative_list: nil, miner_factory: nil,
286
+ event_stream: spine.event_stream
287
+ )
288
+ executor.harvest_engine = Insika::HarvestEngine.new(
289
+ store: spine.backend, harvest_store: spine.harvest_store,
290
+ session_store: spine.session_store, runner: run_harvest,
291
+ profiles: profiles, window: Insika::HarvestEngine::DEFAULT_WINDOW
292
+ )
293
+ # WS6 operator alerts: answers budget_warning / breaker_open /
294
+ # delivery_failed per agent (`alerts.webhook`) via the outbox+claim
295
+ # pipeline. Always wired — the per-agent data gates it (parity).
296
+ executor.alert_dispatcher = Insika::AlertDispatcher.new(
297
+ event_stream: spine.event_stream, outbox: spine.outbox_store,
298
+ channels: spine.channel_registry, profiles: profiles,
299
+ task_store: spine.task_store, http: Insika::HttpClient.new
300
+ )
126
301
 
127
302
  Graph::Result.new(
128
303
  backend: spine.backend, event_stream: spine.event_stream,
129
304
  session_store: spine.session_store, task_store: spine.task_store,
130
305
  checkpoint_store: spine.checkpoint_store, pending_action_store: spine.pending_action_store,
131
306
  delegation_store: spine.delegation_store,
132
- memory_store: spine.memory_store, refinement_store: spine.refinement_store,
133
- outbox_store: spine.outbox_store, inbound_log: spine.inbound_log,
307
+ memory_store: spine.memory_store, memory_audit_store: spine.memory_audit_store,
308
+ refinement_store: spine.refinement_store,
309
+ harvest_store: spine.harvest_store,
310
+ outbox_store: spine.outbox_store, shadow_pair_store: spine.shadow_pair_store,
311
+ inbound_log: spine.inbound_log,
312
+ outcome_store: spine.outcome_store,
313
+ funnel_store: spine.funnel_store,
314
+ contact_store: spine.contact_store,
315
+ followup_store: spine.followup_store,
316
+ proposal_store: spine.proposal_store,
317
+ token_store: spine.token_store, budget_ledger: spine.budget_ledger,
318
+ circuit_state: spine.circuit_state,
134
319
  channel_registry: spine.channel_registry, channel_delivery: channel_delivery,
135
320
  code_tool_registry: spine.code_tool_registry,
136
321
  tool_registry: tool_registry, workflow_registry: spine.workflow_registry,
@@ -144,8 +329,9 @@ module Insika
144
329
 
145
330
  # The CORE command surface every root needs — turn essentials + the operator
146
331
  # controls (pause/approve) the Studio dispatches. Registering pause_task/
147
- # approve_action HERE is the crux of B4: it retires the config.ru:28-34 patch.
148
- def build_core_bus(spine:, profiles:, executor:)
332
+ # approve_action HERE is the crux of: it retires the config.ru:28-34 patch.
333
+ def build_core_bus(spine:, profiles:, executor:, executor_extra: {}, skill_catalog: nil,
334
+ run_distillation: nil)
149
335
  bus = Insika::CommandBus.new
150
336
  bus.register(:create_session,
151
337
  Insika::Commands::CreateSession.new(session_store: spine.session_store, event_stream: spine.event_stream))
@@ -159,10 +345,154 @@ module Insika
159
345
  bus.register(:send_message,
160
346
  Insika::Commands::SendMessage.new(profiles: profiles, session_store: spine.session_store,
161
347
  task_store: spine.task_store, executor: executor,
162
- inbound_log: spine.inbound_log))
348
+ inbound_log: spine.inbound_log,
349
+ contact_store: spine.contact_store,
350
+ followup_store: spine.followup_store,
351
+ store: spine.backend))
163
352
  bus.register(:resume_task,
164
353
  Insika::Commands::ResumeTask.new(profiles: profiles, task_store: spine.task_store,
165
354
  checkpoint_store: spine.checkpoint_store, executor: executor))
355
+ # WS1 multi-tenant credentials: per-tenant + operator tokens, stored as
356
+ # hashes. Operator-only BY CONSTRUCTION — the edge refuses a tenant
357
+ # principal on POST /v1/commands, and the handlers re-check meta.
358
+ bus.register(:issue_tenant_token,
359
+ Insika::Commands::IssueTenantToken.new(token_store: spine.token_store,
360
+ event_stream: spine.event_stream))
361
+ bus.register(:revoke_token,
362
+ Insika::Commands::RevokeToken.new(token_store: spine.token_store,
363
+ event_stream: spine.event_stream))
364
+ bus.register(:rotate_tenant_token,
365
+ Insika::Commands::RotateTenantToken.new(token_store: spine.token_store,
366
+ event_stream: spine.event_stream))
367
+ # WS7: a business outcome per conversation (operator or integration).
368
+ bus.register(:record_outcome,
369
+ Insika::Commands::RecordOutcome.new(outcome_store: spine.outcome_store,
370
+ event_stream: spine.event_stream))
371
+ # the incumbent's half of a shadow pair, one command behind
372
+ # both mirror shapes (the mirror call itself + the follow-up route).
373
+ bus.register(:record_shadow_reply,
374
+ Insika::Commands::RecordShadowReply.new(shadow_pairs: spine.shadow_pair_store,
375
+ event_stream: spine.event_stream))
376
+ # WS8 (LGPD): purge one customer's memory + the whole footprint of their
377
+ # sessions (traces, tasks, checkpoints, outbox). The trace stores are
378
+ # deployment components (nil at the base — skipped). the
379
+ # audit collaborator records a content-free purge line.
380
+ bus.register(:forget_customer,
381
+ Insika::Commands::ForgetCustomer.new(
382
+ memory_store: spine.memory_store, session_store: spine.session_store,
383
+ tool_trace_store: executor_extra[:tool_trace_store],
384
+ context_trace_store: executor_extra[:context_trace_store],
385
+ # the model-visible traces die with the
386
+ # checkpoints (the same SessionPurge list).
387
+ model_visible_trace_store: Insika::ModelVisibleTraceStore.new(store: spine.backend),
388
+ task_store: spine.task_store, checkpoint_store: spine.checkpoint_store,
389
+ outbox_store: spine.outbox_store,
390
+ shadow_pairs: spine.shadow_pair_store,
391
+ audit_store: spine.memory_audit_store,
392
+ followup_store: spine.followup_store,
393
+ contact_store: spine.contact_store,
394
+ proposal_store: spine.proposal_store,
395
+ event_stream: spine.event_stream
396
+ ))
397
+ # the LGPD access right — export one customer's memory
398
+ # cell as content (the Studio download); the event stays counts-only.
399
+ bus.register(:export_customer_memory,
400
+ Insika::Commands::ExportCustomerMemory.new(
401
+ memory_store: spine.memory_store, event_stream: spine.event_stream
402
+ ))
403
+ # WS8 (LGPD): purge ONE TENANT's data — sessions and their footprint,
404
+ # memory cells and outcomes. Operator-only by construction (ingress).
405
+ bus.register(:delete_tenant_data,
406
+ Insika::Commands::DeleteTenantData.new(
407
+ memory_store: spine.memory_store, session_store: spine.session_store,
408
+ tool_trace_store: executor_extra[:tool_trace_store],
409
+ context_trace_store: executor_extra[:context_trace_store],
410
+ # the model-visible traces die with the tenant.
411
+ model_visible_trace_store: Insika::ModelVisibleTraceStore.new(store: spine.backend),
412
+ outcome_store: spine.outcome_store,
413
+ funnel_store: spine.funnel_store, # the fold dies with the tenant
414
+ followup_store: spine.followup_store,
415
+ contact_store: spine.contact_store,
416
+ proposal_store: spine.proposal_store,
417
+ harvest_store: spine.harvest_store,
418
+ task_store: spine.task_store, checkpoint_store: spine.checkpoint_store,
419
+ outbox_store: spine.outbox_store,
420
+ shadow_pairs: spine.shadow_pair_store,
421
+ token_store: spine.token_store, # revoked BEFORE the sweep
422
+ event_stream: spine.event_stream
423
+ ))
424
+ # the operator's baseline freeze — the number
425
+ # read. Synchronous control command, dispatched from the Studio.
426
+ bus.register(:freeze_funnel_baseline,
427
+ Insika::Commands::FreezeFunnelBaseline.new(
428
+ funnel_store: spine.funnel_store, profiles: profiles,
429
+ event_stream: spine.event_stream
430
+ ))
431
+ # the follow-up mutations — the Studio's Cancel button
432
+ # and the channel opt-out event (a tenant principal never reaches the
433
+ # generic command ingress; these ride the same operator-grade path).
434
+ bus.register(:cancel_followup,
435
+ Insika::Commands::CancelFollowup.new(followup_store: spine.followup_store,
436
+ event_stream: spine.event_stream))
437
+ bus.register(:revoke_contact,
438
+ Insika::Commands::RevokeContact.new(contact_store: spine.contact_store,
439
+ followup_store: spine.followup_store,
440
+ store: spine.backend,
441
+ event_stream: spine.event_stream))
442
+ # the human's answer on a distilled proposal — the ONLY
443
+ # mutation behind the Facts (wiki) page. Synchronous control command.
444
+ bus.register(:resolve_proposal,
445
+ Insika::Commands::ResolveProposal.new(
446
+ proposal_store: spine.proposal_store,
447
+ memory_store: spine.memory_store,
448
+ event_stream: spine.event_stream
449
+ ))
450
+ # /C10: the gated harvest — the mining pass, the double
451
+ # gate, the human's promote/rollback/dismiss. The BASE graph wires the
452
+ # nil factories (no eval surface, no criterion, no funnel): every flow
453
+ # refuses with a named reason or skips (parity). The deployment root
454
+ # re-registers with the real gate/criterion/negative list.
455
+ bus.register(:run_harvest,
456
+ Insika::Commands::RunHarvest.new(
457
+ profiles: profiles, harvest_store: spine.harvest_store,
458
+ session_store: spine.session_store, task_store: spine.task_store,
459
+ skill_store: skill_catalog.store,
460
+ tool_trace_store: executor_extra[:tool_trace_store],
461
+ settings_store: executor_extra[:settings_store],
462
+ negative_list: nil, miner_factory: nil,
463
+ event_stream: spine.event_stream
464
+ ))
465
+ bus.register(:gate_harvest,
466
+ Insika::Commands::GateHarvest.new(
467
+ harvest_store: spine.harvest_store, gate: nil,
468
+ conversion_gate: nil, criterion: nil,
469
+ event_stream: spine.event_stream
470
+ ))
471
+ bus.register(:promote_harvest,
472
+ Insika::Commands::PromoteHarvest.new(
473
+ harvest_store: spine.harvest_store,
474
+ skill_store: skill_catalog.store,
475
+ skill_catalog: skill_catalog,
476
+ profile_source: profiles, criterion: nil, conversion_gate: nil,
477
+ event_stream: spine.event_stream
478
+ ))
479
+ bus.register(:rollback_harvest,
480
+ Insika::Commands::RollbackHarvest.new(
481
+ harvest_store: spine.harvest_store,
482
+ skill_store: skill_catalog.store,
483
+ skill_catalog: skill_catalog,
484
+ profile_source: profiles, event_stream: spine.event_stream
485
+ ))
486
+ bus.register(:reject_harvest,
487
+ Insika::Commands::RejectHarvest.new(
488
+ harvest_store: spine.harvest_store, event_stream: spine.event_stream
489
+ ))
490
+ # the ONE path that writes proposals, served to the
491
+ # Studio's "Run distillation now" button. The same instance the
492
+ # DistillEngine's loop uses, so the manual and the automatic share one
493
+ # code path; the engine keeps the automatic pass when the root does
494
+ # not pass a runner here (nil = the command is not on the bus).
495
+ bus.register(:run_distillation, run_distillation) if run_distillation
166
496
  bus
167
497
  end
168
498
 
@@ -170,9 +500,13 @@ module Insika
170
500
  # them to their historic public constants (SESSION_STORE, REGISTRY, ...).
171
501
  Spine = Struct.new(
172
502
  :backend, :event_stream, :session_store, :task_store, :checkpoint_store,
173
- :pending_action_store, :delegation_store, :memory_store, :refinement_store,
174
- :outbox_store, :inbound_log, :code_tool_registry,
175
- :workflow_registry, :policy_registry, :capability_registry, :hooks,
503
+ :pending_action_store, :delegation_store, :memory_store, :memory_audit_store,
504
+ :refinement_store,
505
+ :harvest_store,
506
+ :token_store, :budget_ledger, :circuit_state, :outbox_store, :shadow_pair_store,
507
+ :inbound_log, :outcome_store, :funnel_store,
508
+ :contact_store, :followup_store, :proposal_store,
509
+ :code_tool_registry, :workflow_registry, :policy_registry, :capability_registry, :hooks,
176
510
  :channel_registry, keyword_init: true
177
511
  )
178
512
 
@@ -182,7 +516,11 @@ module Insika
182
516
  Result = Struct.new(
183
517
  :backend, :event_stream,
184
518
  :session_store, :task_store, :checkpoint_store, :pending_action_store, :delegation_store,
185
- :memory_store, :refinement_store, :outbox_store, :inbound_log,
519
+ :memory_store, :memory_audit_store, :refinement_store, :outbox_store, :shadow_pair_store,
520
+ :inbound_log, :token_store,
521
+ :budget_ledger, :circuit_state, :outcome_store, :funnel_store,
522
+ :contact_store, :followup_store, :proposal_store,
523
+ :harvest_store,
186
524
  :channel_registry, :channel_delivery,
187
525
  :code_tool_registry, :tool_registry, :workflow_registry, :policy_registry, :capability_registry,
188
526
  :tool_catalog, :skill_catalog, :prompt_catalog,
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Insika
4
- # Workflow surface (item 22 / COMPETITIVE-ANALYSIS §4.4). A workflow is a Ruby
4
+ # Workflow surface (COMPETITIVE-ANALYSIS). A workflow is a Ruby
5
5
  # callable orchestrating RubyLLM Agents/Workflows FROM WITHIN (RubyLLM First);
6
6
  # its durable RUN is a Task (checkpointed, recoverable — stronger than Flue's
7
7
  # at-most-once run record). This module adds the EXPOSED surface the consumer
@@ -8,7 +8,7 @@ module Insika
8
8
  # checkpoint at the end. Nothing is validated at registration (the callable may come
9
9
  # via a factory block). Consumed by the TriggerWorkflow handler.
10
10
  #
11
- # Item 22 / §4.4 — the EXPOSED surface: a registration may carry `input_schema:` /
11
+ # — the EXPOSED surface: a registration may carry `input_schema:` /
12
12
  # `output_schema:` (a JSON Schema Hash or any dry-schema-compatible validator) and a
13
13
  # `description:` in its metadata. `definition` bundles them with the factory into a
14
14
  # Workflow::Definition (schemas resolved, factory left lazy); `catalog` is the