insika 0.2.0 → 0.7.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 (300) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +325 -17
  3. data/README.md +52 -11
  4. data/bin/insika +895 -8
  5. data/bin/insika-router +87 -0
  6. data/docs/AGENTS.md +229 -163
  7. data/docs/API.md +56 -0
  8. data/docs/ARCHITECTURE.md +3 -2
  9. data/docs/ARTIFACTS.md +95 -0
  10. data/docs/BENCHMARK.md +2 -2
  11. data/docs/CHANNELS.md +111 -21
  12. data/docs/CONTEXT.md +132 -19
  13. data/docs/DEMO.md +80 -0
  14. data/docs/DEPLOY.md +75 -4
  15. data/docs/EMBEDDING.md +1 -1
  16. data/docs/EVALS.md +146 -2
  17. data/docs/FACTS.md +135 -0
  18. data/docs/HARVEST.md +116 -0
  19. data/docs/KNOWLEDGE.md +290 -0
  20. data/docs/LOADTEST.md +16 -10
  21. data/docs/MEDIA.md +128 -0
  22. data/docs/OBSERVABILITY.md +65 -6
  23. data/docs/OUTCOMES.md +137 -0
  24. data/docs/PLUGINS.md +51 -6
  25. data/docs/POLICY.md +216 -0
  26. data/docs/REFINEMENT.md +17 -12
  27. data/docs/RELEASING.md +27 -0
  28. data/docs/ROUTER.md +213 -0
  29. data/docs/RUNNING-LOCAL.md +6 -6
  30. data/docs/SCHEDULING.md +121 -0
  31. data/docs/SECURITY.md +79 -2
  32. data/docs/SKILLS.md +13 -4
  33. data/docs/SOAK.md +127 -0
  34. data/docs/TEMPLATES.md +134 -0
  35. data/docs/TOOLS.md +220 -27
  36. data/docs/WHY.md +1 -1
  37. data/docs/WORKFLOWS.md +2 -2
  38. data/docs/_includes/head_custom.html +5 -0
  39. data/docs/_includes/title.html +13 -0
  40. data/docs/_sass/color_schemes/insika.scss +32 -0
  41. data/docs/_sass/custom/custom.scss +199 -0
  42. data/docs/_sass/custom/setup.scss +26 -0
  43. data/docs/assets/img/favicon.svg +7 -0
  44. data/docs/assets/img/insika-mark.svg +7 -0
  45. data/docs/core-concepts.md +21 -0
  46. data/docs/domain.md +115 -0
  47. data/docs/improve.md +20 -0
  48. data/docs/index.md +9 -6
  49. data/docs/integrate.md +20 -0
  50. data/docs/operate.md +13 -6
  51. data/docs/prompts/ADD-TOOL.md +118 -0
  52. data/docs/prompts/DIAGNOSE-TURN.md +65 -0
  53. data/docs/prompts/GO-LIVE.md +138 -0
  54. data/docs/prompts/RUN-EXAMPLES.md +70 -0
  55. data/docs/reference.md +19 -0
  56. data/docs/ship.md +10 -2
  57. data/docs/start-here.md +18 -0
  58. data/lib/insika/agent_profile.rb +202 -10
  59. data/lib/insika/artifact_signing.rb +82 -0
  60. data/lib/insika/artifact_store.rb +160 -0
  61. data/lib/insika/balloon_splitter.rb +102 -0
  62. data/lib/insika/budget_ledger.rb +34 -3
  63. data/lib/insika/cache_series_store.rb +49 -0
  64. data/lib/insika/channel_delivery.rb +119 -14
  65. data/lib/insika/channels/relay.rb +77 -3
  66. data/lib/insika/channels/web.rb +2 -2
  67. data/lib/insika/chat_builder.rb +105 -2
  68. data/lib/insika/checkpoint_store.rb +16 -0
  69. data/lib/insika/commands/agent_payload.rb +3 -3
  70. data/lib/insika/commands/backfill_knowledge.rb +145 -0
  71. data/lib/insika/commands/cancel_followup.rb +49 -0
  72. data/lib/insika/commands/delete_artifact.rb +35 -0
  73. data/lib/insika/commands/delete_concept.rb +34 -0
  74. data/lib/insika/commands/delete_mcp.rb +6 -2
  75. data/lib/insika/commands/delete_tenant_data.rb +107 -0
  76. data/lib/insika/commands/export_customer_memory.rb +48 -0
  77. data/lib/insika/commands/forget_customer.rb +117 -0
  78. data/lib/insika/commands/freeze_funnel_baseline.rb +113 -0
  79. data/lib/insika/commands/gate_harvest.rb +138 -0
  80. data/lib/insika/commands/gate_refinement.rb +1 -1
  81. data/lib/insika/commands/judge_shadow_pairs.rb +124 -0
  82. data/lib/insika/commands/memory_forget_fact.rb +20 -4
  83. data/lib/insika/commands/memory_put_fact.rb +23 -4
  84. data/lib/insika/commands/promote_harvest.rb +130 -0
  85. data/lib/insika/commands/record_outcome.rb +46 -0
  86. data/lib/insika/commands/record_shadow_reply.rb +68 -0
  87. data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
  88. data/lib/insika/commands/reject_harvest.rb +38 -0
  89. data/lib/insika/commands/resolve_proposal.rb +108 -0
  90. data/lib/insika/commands/restore_concept.rb +34 -0
  91. data/lib/insika/commands/revoke_contact.rb +49 -0
  92. data/lib/insika/commands/rollback_harvest.rb +86 -0
  93. data/lib/insika/commands/run_distillation.rb +186 -0
  94. data/lib/insika/commands/run_harvest.rb +393 -0
  95. data/lib/insika/commands/seed_demo_data.rb +31 -0
  96. data/lib/insika/commands/send_message.rb +103 -6
  97. data/lib/insika/commands/session_purge.rb +67 -0
  98. data/lib/insika/commands/upsert_mcp.rb +6 -3
  99. data/lib/insika/commands/write_concept.rb +57 -0
  100. data/lib/insika/contact_store.rb +183 -0
  101. data/lib/insika/context/builder.rb +21 -3
  102. data/lib/insika/context/fragment.rb +7 -3
  103. data/lib/insika/context/priority.rb +5 -0
  104. data/lib/insika/context/provider.rb +17 -3
  105. data/lib/insika/context/providers/briefing.rb +96 -0
  106. data/lib/insika/context/providers/knowledge.rb +108 -0
  107. data/lib/insika/context/providers/memory.rb +16 -7
  108. data/lib/insika/context/providers/prompt.rb +57 -23
  109. data/lib/insika/context/providers/skill.rb +2 -0
  110. data/lib/insika/context/providers/tool_search.rb +2 -0
  111. data/lib/insika/context_trace_store.rb +38 -2
  112. data/lib/insika/cron.rb +189 -0
  113. data/lib/insika/demo/agent_attrs.rb +43 -0
  114. data/lib/insika/demo/golden_cases.rb +81 -0
  115. data/lib/insika/demo/seeder.rb +336 -0
  116. data/lib/insika/distill.rb +224 -0
  117. data/lib/insika/distill_engine.rb +169 -0
  118. data/lib/insika/doctor.rb +889 -11
  119. data/lib/insika/dsl/definition.rb +3 -2
  120. data/lib/insika/dsl/runtime.rb +69 -82
  121. data/lib/insika/dsl/server_boot.rb +92 -1
  122. data/lib/insika/dsl/system.rb +10 -2
  123. data/lib/insika/dsl.rb +186 -5
  124. data/lib/insika/edge_limiter.rb +28 -7
  125. data/lib/insika/env_schema.rb +25 -3
  126. data/lib/insika/errors.rb +11 -0
  127. data/lib/insika/evals/assertions.rb +3 -2
  128. data/lib/insika/evals/golden.rb +41 -4
  129. data/lib/insika/evals/judge.rb +47 -2
  130. data/lib/insika/evals/pairwise.rb +29 -6
  131. data/lib/insika/evals/persona.rb +98 -0
  132. data/lib/insika/evals/runner.rb +9 -0
  133. data/lib/insika/evals/simulator.rb +225 -0
  134. data/lib/insika/evals/transport.rb +83 -1
  135. data/lib/insika/event_stream.rb +10 -0
  136. data/lib/insika/evidence.rb +183 -0
  137. data/lib/insika/executor.rb +869 -76
  138. data/lib/insika/followup_engine.rb +207 -0
  139. data/lib/insika/followup_policy.rb +198 -0
  140. data/lib/insika/followup_store.rb +306 -0
  141. data/lib/insika/funnel_declaration.rb +106 -0
  142. data/lib/insika/funnel_fold.rb +179 -0
  143. data/lib/insika/funnel_store.rb +163 -0
  144. data/lib/insika/golden_store.rb +17 -2
  145. data/lib/insika/grounding/matcher.rb +69 -0
  146. data/lib/insika/grounding.rb +44 -0
  147. data/lib/insika/harvest/conversion_gate.rb +159 -0
  148. data/lib/insika/harvest/criterion.rb +98 -0
  149. data/lib/insika/harvest/gate.rb +194 -0
  150. data/lib/insika/harvest/negative_list.rb +199 -0
  151. data/lib/insika/harvest.rb +241 -0
  152. data/lib/insika/harvest_engine.rb +193 -0
  153. data/lib/insika/harvest_store.rb +548 -0
  154. data/lib/insika/knowledge.rb +680 -0
  155. data/lib/insika/knowledge_store.rb +140 -0
  156. data/lib/insika/mcp_client.rb +94 -0
  157. data/lib/insika/mcp_json.rb +74 -0
  158. data/lib/insika/mcp_live_tool.rb +43 -0
  159. data/lib/insika/mcp_store.rb +98 -26
  160. data/lib/insika/mcp_tool_ingestor.rb +30 -8
  161. data/lib/insika/mcp_tool_registry.rb +100 -0
  162. data/lib/insika/media.rb +382 -0
  163. data/lib/insika/memory_audit_store.rb +85 -0
  164. data/lib/insika/memory_store.rb +264 -23
  165. data/lib/insika/message_origin.rb +7 -2
  166. data/lib/insika/middleware.rb +9 -0
  167. data/lib/insika/model_visible.rb +87 -0
  168. data/lib/insika/model_visible_trace_store.rb +66 -0
  169. data/lib/insika/onboarding.rb +22 -1
  170. data/lib/insika/outbox_store.rb +42 -4
  171. data/lib/insika/outcome_store.rb +147 -0
  172. data/lib/insika/overlay_tool_registry.rb +37 -17
  173. data/lib/insika/packaging.rb +163 -0
  174. data/lib/insika/parity/criterion.rb +79 -0
  175. data/lib/insika/parity/verdict.rb +318 -0
  176. data/lib/insika/prefix_fingerprint.rb +58 -0
  177. data/lib/insika/profile_source.rb +29 -1
  178. data/lib/insika/prompt_catalog.rb +10 -0
  179. data/lib/insika/proposal_store.rb +271 -0
  180. data/lib/insika/queue_policy.rb +4 -1
  181. data/lib/insika/refinement/proposer.rb +1 -1
  182. data/lib/insika/reliability.rb +32 -6
  183. data/lib/insika/retention.rb +316 -0
  184. data/lib/insika/router/app.rb +157 -0
  185. data/lib/insika/router/backend_pool.rb +98 -0
  186. data/lib/insika/router/hash_ring.rb +55 -0
  187. data/lib/insika/router/proxy_body.rb +34 -0
  188. data/lib/insika/router/session_key.rb +54 -0
  189. data/lib/insika/router.rb +18 -0
  190. data/lib/insika/routing.rb +101 -0
  191. data/lib/insika/safety/config.rb +44 -4
  192. data/lib/insika/safety/corpus.rb +255 -0
  193. data/lib/insika/safety/detectors.rb +32 -113
  194. data/lib/insika/safety/factory.rb +15 -2
  195. data/lib/insika/safety/grounding_enforcer.rb +59 -0
  196. data/lib/insika/safety/grounding_validator.rb +49 -0
  197. data/lib/insika/safety/input_guardrail.rb +1 -1
  198. data/lib/insika/safety/moderator.rb +1 -1
  199. data/lib/insika/safety/output_filter.rb +9 -5
  200. data/lib/insika/safety/output_validator.rb +11 -5
  201. data/lib/insika/schedule.rb +177 -0
  202. data/lib/insika/schedule_engine.rb +314 -0
  203. data/lib/insika/schedule_store.rb +208 -0
  204. data/lib/insika/schema_guard.rb +35 -0
  205. data/lib/insika/server/app.rb +247 -17
  206. data/lib/insika/server/rack_app.rb +21 -1
  207. data/lib/insika/server/responses.rb +40 -1
  208. data/lib/insika/session_actor.rb +8 -4
  209. data/lib/insika/session_store.rb +65 -2
  210. data/lib/insika/settings_store.rb +10 -0
  211. data/lib/insika/shadow_pair_store.rb +258 -0
  212. data/lib/insika/skill_catalog.rb +16 -0
  213. data/lib/insika/soak/envelope.rb +140 -0
  214. data/lib/insika/soak/report.rb +392 -0
  215. data/lib/insika/soak/runner.rb +554 -0
  216. data/lib/insika/steer_injector.rb +21 -10
  217. data/lib/insika/store.rb +10 -1
  218. data/lib/insika/stores/memory.rb +6 -0
  219. data/lib/insika/stores/sqlite.rb +8 -0
  220. data/lib/insika/studio/app.rb +1437 -54
  221. data/lib/insika/studio/assets/dist/application.css +1 -1
  222. data/lib/insika/studio/assets/dist/application.js +23 -23
  223. data/lib/insika/studio/assets/dist/favicon.svg +6 -0
  224. data/lib/insika/studio/forms.rb +308 -10
  225. data/lib/insika/studio/nav_icons.rb +19 -1
  226. data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
  227. data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
  228. data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
  229. data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
  230. data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
  231. data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
  232. data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
  233. data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
  234. data/lib/insika/studio/views/_agents_master.erb +44 -0
  235. data/lib/insika/studio/views/_message.erb +49 -32
  236. data/lib/insika/studio/views/agent_detail.erb +62 -278
  237. data/lib/insika/studio/views/agents.erb +71 -54
  238. data/lib/insika/studio/views/approvals.erb +4 -1
  239. data/lib/insika/studio/views/artifact.erb +23 -0
  240. data/lib/insika/studio/views/artifacts.erb +59 -0
  241. data/lib/insika/studio/views/chats.erb +4 -1
  242. data/lib/insika/studio/views/customer.erb +94 -0
  243. data/lib/insika/studio/views/customers.erb +32 -0
  244. data/lib/insika/studio/views/evals.erb +6 -3
  245. data/lib/insika/studio/views/facts.erb +133 -0
  246. data/lib/insika/studio/views/followups.erb +125 -0
  247. data/lib/insika/studio/views/funnel.erb +106 -0
  248. data/lib/insika/studio/views/harvest.erb +234 -0
  249. data/lib/insika/studio/views/home.erb +106 -66
  250. data/lib/insika/studio/views/knowledge.erb +123 -0
  251. data/lib/insika/studio/views/layout.erb +15 -11
  252. data/lib/insika/studio/views/mcp.erb +174 -80
  253. data/lib/insika/studio/views/parity.erb +147 -0
  254. data/lib/insika/studio/views/playground.erb +7 -1
  255. data/lib/insika/studio/views/session.erb +233 -124
  256. data/lib/insika/studio/views/settings.erb +41 -1
  257. data/lib/insika/studio/views/skills.erb +1 -2
  258. data/lib/insika/studio/views/system_files.erb +1 -1
  259. data/lib/insika/studio/views/task.erb +13 -0
  260. data/lib/insika/studio/views/tasks.erb +4 -1
  261. data/lib/insika/studio/views/tools.erb +24 -10
  262. data/lib/insika/task_store.rb +21 -1
  263. data/lib/insika/templates/browser-agent/README.md +36 -0
  264. data/lib/insika/templates/browser-agent/agent.rb +49 -0
  265. data/lib/insika/templates/daily-digest/README.md +38 -0
  266. data/lib/insika/templates/daily-digest/agent.rb +77 -0
  267. data/lib/insika/templates/repo-explorer/README.md +36 -0
  268. data/lib/insika/templates/repo-explorer/agent.rb +45 -0
  269. data/lib/insika/templates/research-analyst/README.md +26 -0
  270. data/lib/insika/templates/research-analyst/agent.rb +58 -0
  271. data/lib/insika/templates/review-panel/README.md +20 -0
  272. data/lib/insika/templates/review-panel/agent.rb +50 -0
  273. data/lib/insika/templates/travel-planner/README.md +35 -0
  274. data/lib/insika/templates/travel-planner/agent.rb +87 -0
  275. data/lib/insika/templates.rb +112 -0
  276. data/lib/insika/testing/store_contract.rb +27 -6
  277. data/lib/insika/tick.rb +47 -11
  278. data/lib/insika/timezone.rb +45 -0
  279. data/lib/insika/tool_definition.rb +17 -7
  280. data/lib/insika/tool_envelope.rb +69 -0
  281. data/lib/insika/tool_manifest.rb +5 -1
  282. data/lib/insika/tools/data_defined_tool.rb +10 -0
  283. data/lib/insika/tools/generate_image.rb +89 -0
  284. data/lib/insika/tools/load_knowledge.rb +74 -0
  285. data/lib/insika/tools/run_persona_eval.rb +328 -0
  286. data/lib/insika/tools/save_artifact.rb +95 -0
  287. data/lib/insika/tools/schedule_followup.rb +164 -0
  288. data/lib/insika/tools/tts.rb +47 -0
  289. data/lib/insika/tools/update_briefing.rb +126 -0
  290. data/lib/insika/turn_output.rb +1 -1
  291. data/lib/insika/turn_state.rb +49 -1
  292. data/lib/insika/turn_timing.rb +22 -2
  293. data/lib/insika/version.rb +1 -1
  294. data/lib/insika/vitals.rb +84 -0
  295. data/lib/insika/wiring/graph.rb +472 -13
  296. data/lib/insika/wiring/graph_chat.rb +102 -0
  297. data/lib/insika.rb +149 -3
  298. metadata +181 -5
  299. data/docs/build.md +0 -14
  300. data/docs/understand.md +0 -10
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "graph_chat"
4
+
3
5
  module Insika
4
6
  module Wiring
5
7
  # SHARED composition core for both roots: the minimal wiring
@@ -45,6 +47,10 @@ module Insika
45
47
  pending_action_store = Insika::PendingActionStore.new(store: backend)
46
48
  delegation_store = Insika::DelegationStore.new(store: backend)
47
49
  memory_store = Insika::MemoryStore.new(store: backend)
50
+ # the append-only, content-free audit of OPERATOR memory
51
+ # mutations. Built unconditionally (empty and free when nothing writes)
52
+ # so a deployment that turns it on later finds its audit already durable.
53
+ memory_audit_store = Insika::MemoryAuditStore.new(store: backend)
48
54
  token_store = Insika::TokenStore.new(store: backend)
49
55
  budget_ledger = Insika::BudgetLedger.new(store: backend)
50
56
  circuit_state = Insika::CircuitState.new(store: backend)
@@ -54,11 +60,56 @@ module Insika
54
60
  # (they are empty and free when no channel is registered) so a deployment
55
61
  # that turns a channel on later finds its state already durable.
56
62
  outbox_store = Insika::OutboxStore.new(store: backend)
63
+ # the shadow pair store — one durable record per mirrored
64
+ # exchange, written by our half and the incumbent's. Built unconditionally
65
+ # (empty and free when shadow is off) so turning it on later finds the
66
+ # state already durable, beside the outbox on the same runtime backend.
67
+ shadow_pair_store = Insika::ShadowPairStore.new(store: backend)
57
68
  inbound_log = Insika::InboundLog.new(store: backend)
69
+ # WS7: business outcomes per conversation, recorded by the operator or
70
+ # the integration (POST /v1/outcomes). Built unconditionally (empty and
71
+ # free when nothing records) so the Studio's scorecard always has a store.
72
+ outcome_store = Insika::OutcomeStore.new(store: backend)
73
+ # the funnel's durable aggregates — per-day stage counts,
74
+ # fold cursors and baseline snapshots. Built unconditionally (empty and
75
+ # free when no pack declares a funnel) so the fold, the doctor and the
76
+ # Studio always have a store to read/write.
77
+ funnel_store = Insika::FunnelStore.new(store: backend)
78
+ # the contact-state cells and the follow-up schedule
79
+ # records. Built unconditionally (empty and free when no pack declares
80
+ # followup) so the engine, the tools and the Studio always have stores.
81
+ contact_store = Insika::ContactStore.new(store: backend)
82
+ followup_store = Insika::FollowupStore.new(store: backend)
83
+ # the recurring-schedule rows: one row per
84
+ # declared schedule, declaration + runtime state. Built unconditionally
85
+ # (empty and free when no pack declares schedules) so the engine, the
86
+ # doctor and the Studio always have a store.
87
+ schedule_store = Insika::ScheduleStore.new(store: backend)
88
+ # the distilled proposals + the latched dedup ledger +
89
+ # the per-session markers. Built unconditionally (empty and free when
90
+ # no pack declares distill) so the wiki, the engine and the LGPD
91
+ # purges always have a store.
92
+ proposal_store = Insika::ProposalStore.new(store: backend)
93
+ # concepts extracted from finished turns. Built
94
+ # unconditionally (empty and free when no pack declares knowledge)
95
+ # so the extractor, the Studio and the LGPD purges always have a
96
+ # store.
97
+ knowledge_store = Insika::KnowledgeStore.new(store: backend)
58
98
  # refinement RUNS (reports over real traffic). Runtime data,
59
99
  # same backend as sessions/tasks — the collector and the command that write it
60
100
  # are the root's business (deployment-only, like the memory commands).
61
101
  refinement_store = Insika::RefinementStore.new(store: backend)
102
+ # the harvest's durable half — mining runs, the per-
103
+ # candidate lifecycle, the append-only promotion log, the snapshots
104
+ # and the per-session markers. Built unconditionally (empty and free
105
+ # when no pack declares harvest) so the engine, the Studio and the
106
+ # LGPD purges always have a store.
107
+ harvest_store = Insika::HarvestStore.new(store: backend)
108
+ # the report destination — one record per run, no
109
+ # versioning. Built unconditionally (empty and free when nothing saves)
110
+ # so the tool, the routes, the retention sweep and the tenant purge
111
+ # always have a store.
112
+ artifact_store = Insika::ArtifactStore.new(store: backend)
62
113
 
63
114
  code_tool_registry = Insika::ToolRegistry.new
64
115
  workflow_registry = Insika::WorkflowRegistry.new
@@ -74,9 +125,20 @@ module Insika
74
125
  session_store: session_store, task_store: task_store,
75
126
  checkpoint_store: checkpoint_store, pending_action_store: pending_action_store,
76
127
  delegation_store: delegation_store,
77
- memory_store: memory_store, refinement_store: refinement_store,
128
+ memory_store: memory_store, memory_audit_store: memory_audit_store,
129
+ refinement_store: refinement_store,
130
+ harvest_store: harvest_store,
78
131
  token_store: token_store, budget_ledger: budget_ledger, circuit_state: circuit_state,
79
- outbox_store: outbox_store, inbound_log: inbound_log,
132
+ outbox_store: outbox_store, shadow_pair_store: shadow_pair_store,
133
+ inbound_log: inbound_log,
134
+ outcome_store: outcome_store,
135
+ funnel_store: funnel_store,
136
+ contact_store: contact_store,
137
+ followup_store: followup_store,
138
+ schedule_store: schedule_store,
139
+ proposal_store: proposal_store,
140
+ knowledge_store: knowledge_store,
141
+ artifact_store: artifact_store,
80
142
  code_tool_registry: code_tool_registry,
81
143
  workflow_registry: workflow_registry, policy_registry: policy_registry,
82
144
  capability_registry: Insika::CapabilityRegistry.new, hooks: Insika::Hooks.new,
@@ -99,6 +161,13 @@ module Insika
99
161
  def build(spine:, profiles:, tool_registry:, tool_catalog:, skill_catalog:,
100
162
  prompt_catalog:, guardrails:, context_providers:, edge_limiter: nil,
101
163
  executor_extra: {})
164
+ # the save_artifact tool: a REGISTRY tool registered OPTIONAL (the
165
+ # per-agent allowlist is the switch — an agent that did not name it
166
+ # cannot call it). Registered in the shared code registry, so both
167
+ # roots (base registry + the deployment's overlay, which composes it)
168
+ # expose it. The gem's require lives IN the factory block (loaded on
169
+ # the 1st instance, turn time -> wiring-load stays gem-free).
170
+ register_artifact_tool(spine)
102
171
  spine.hooks.register(:task, after: guardrails.output_validator)
103
172
  middleware = Insika::MiddlewareStack.new([edge_limiter, guardrails.input_guardrail].compact)
104
173
 
@@ -115,7 +184,8 @@ module Insika
115
184
  # only production exercises.
116
185
  channel_delivery = Insika::ChannelDelivery.new(
117
186
  channels: spine.channel_registry, outbox: spine.outbox_store,
118
- session_store: spine.session_store, event_stream: spine.event_stream
187
+ session_store: spine.session_store, event_stream: spine.event_stream,
188
+ shadow_pairs: spine.shadow_pair_store
119
189
  )
120
190
 
121
191
  # WS3 provider reliability (retries/backoff/fallback/breaker) is ALWAYS wired —
@@ -135,23 +205,132 @@ module Insika
135
205
  delegation_store: spine.delegation_store, # async delegation durability
136
206
  channel_delivery: channel_delivery, # out-of-band reply delivery
137
207
  reliability: reliability, # WS3: retries/fallback/breaker (data-gated)
208
+ grounding_enforcer: guardrails.grounding_enforcer, # :enforce cut
209
+ # the follow-up stores — the ChatBuilder wires the
210
+ # schedule/cancel_followup tools on them (parity when no pack declares).
211
+ contact_store: spine.contact_store,
212
+ followup_store: spine.followup_store,
213
+ # the model-visible trace rides executor_extra like the
214
+ # context trace — a module graph that omits it still builds (nil =
215
+ # parity). The base graph wires it over the same backend the context
216
+ # trace uses.
217
+ model_visible_trace_store: Insika::ModelVisibleTraceStore.new(store: spine.backend),
218
+ # the per-turn extraction hook (nil = the loop is off,
219
+ # parity). Gated per-agent by `profile.knowledge`.
220
+ knowledge_store: spine.knowledge_store,
138
221
  **executor_extra
139
222
  )
140
223
 
141
- bus = build_core_bus(spine: spine, profiles: profiles, executor: executor)
224
+ # the ONE path that writes proposals, shared by the
225
+ # automatic DistillEngine loop and the bus command behind the Studio's
226
+ # "Run distillation now" button (one code path for both).
227
+ run_distillation = Insika::Commands::RunDistillation.new(
228
+ profiles: profiles, proposal_store: spine.proposal_store,
229
+ session_store: spine.session_store, memory_store: spine.memory_store,
230
+ settings_store: executor_extra[:settings_store],
231
+ event_stream: spine.event_stream
232
+ )
233
+
234
+ bus = build_core_bus(spine: spine, profiles: profiles, executor: executor,
235
+ executor_extra: executor_extra, skill_catalog: skill_catalog,
236
+ run_distillation: run_distillation)
142
237
 
143
238
  # the periodic tick (outbox drain + stale recovery sweep). Built
144
239
  # here, after the bus, because its recovery half dispatches resume_task
145
240
  # through it; handed to the Executor, which starts it as a child of the
146
241
  # turn supervisor in serving mode. `INSIKA_TICK_INTERVAL=0` disables.
242
+ # WS8 retention rides the tick: always built, the settings knob
243
+ # (`retention_days`) gates it — the base graph (no settings_store)
244
+ # reads as OFF.
147
245
  executor.tick = Insika::Tick.new(
148
246
  store: spine.backend, channel_delivery: channel_delivery,
149
247
  recovery: Insika::Recovery.new(
150
248
  task_store: spine.task_store, checkpoint_store: spine.checkpoint_store, command_bus: bus
151
249
  ),
250
+ retention: Insika::Retention.new(
251
+ store: spine.backend, session_store: spine.session_store,
252
+ task_store: spine.task_store, checkpoint_store: spine.checkpoint_store,
253
+ memory_store: spine.memory_store, outcome_store: spine.outcome_store,
254
+ tool_trace_store: executor_extra[:tool_trace_store],
255
+ context_trace_store: executor_extra[:context_trace_store],
256
+ # the model-visible traces die with their checkpoints.
257
+ model_visible_trace_store: Insika::ModelVisibleTraceStore.new(store: spine.backend),
258
+ outbox_store: spine.outbox_store,
259
+ shadow_pair_store: spine.shadow_pair_store,
260
+ settings_store: executor_extra[:settings_store],
261
+ budget_ledger: spine.budget_ledger, # WS2 counter GC (retention-independent)
262
+ funnel_store: spine.funnel_store, # fold dies with its source
263
+ followup_store: spine.followup_store, # records age out too
264
+ contact_store: spine.contact_store, # cells age out too
265
+ proposal_store: spine.proposal_store, # proposals age out too
266
+ harvest_store: spine.harvest_store, # candidates/log/snapshots too
267
+ artifact_store: spine.artifact_store # reports expire on their own TTL
268
+ ),
152
269
  interval: tick_env("INSIKA_TICK_INTERVAL") || Insika::Tick::DEFAULT_INTERVAL,
153
270
  stale_after: tick_env("INSIKA_TICK_STALE_AFTER") || Insika::Tick::DEFAULT_STALE_AFTER
154
271
  )
272
+ # the tick-driven outcome fold — wired here, after the
273
+ # Tick, because it reads the outcome/funnel stores of the spine and the
274
+ # profiles; the per-pair declaration gates it (nil funnel everywhere =
275
+ # an inert fold). Always built: a pack that declares a funnel later
276
+ # finds its fold already on the tick.
277
+ executor.tick.funnel = Insika::FunnelFold.new(
278
+ outcome_store: spine.outcome_store, funnel_store: spine.funnel_store,
279
+ profiles: profiles, store: spine.backend
280
+ )
281
+ # the tick-driven follow-up firer — the tick's third
282
+ # duty, wired here after the Tick, gated by its own claim window.
283
+ # Always built: a pack that declares followup later finds its firer
284
+ # already on the tick (inert when no profile declares followup).
285
+ executor.tick.followup = Insika::FollowupEngine.new(
286
+ store: spine.backend, followup_store: spine.followup_store,
287
+ contact_store: spine.contact_store, task_store: spine.task_store,
288
+ profiles: profiles, executor: executor, event_stream: spine.event_stream
289
+ )
290
+ # the recurring-schedule firer — the tick's
291
+ # fourth duty, wired here after the Tick, gated by its own claim
292
+ # window. Always built: a pack that declares `schedules` later finds
293
+ # its firer already on the tick (inert when no profile declares one).
294
+ executor.tick.schedule = Insika::ScheduleEngine.new(
295
+ store: spine.backend, schedule_store: spine.schedule_store,
296
+ task_store: spine.task_store, session_store: spine.session_store,
297
+ profiles: profiles, executor: executor, budget_ledger: spine.budget_ledger,
298
+ event_stream: spine.event_stream
299
+ )
300
+ # the distillation engine — the tick-duty that finds idle
301
+ # customer sessions and distills them on its own worker fiber (a
302
+ # supervisor child, started in serving mode next to the tick).
303
+ # Always built: a pack that declares `distill:` later finds its engine
304
+ # already wired — and INERT until one does (the engine gates its start
305
+ # and its passes on a declaring profile).
306
+ executor.distill_engine = Insika::DistillEngine.new(
307
+ store: spine.backend, proposal_store: spine.proposal_store,
308
+ session_store: spine.session_store,
309
+ runner: run_distillation,
310
+ profiles: profiles,
311
+ window: Insika::DistillEngine::DEFAULT_WINDOW
312
+ )
313
+ # the harvest engine — the tick-duty that finds idle,
314
+ # unmined sessions whose agent declares `harvest.enabled` and mines
315
+ # them on its own worker fiber (a supervisor child, started in serving
316
+ # mode next to the tick). Always built: INERT until a profile declares
317
+ # harvest (the engine gates its start and its passes on data). The
318
+ # runner is the same RunHarvest the bus serves, so the manual CLI and
319
+ # the automated loop share one code path.
320
+ run_harvest = Insika::Commands::RunHarvest.new(
321
+ profiles: profiles, harvest_store: spine.harvest_store,
322
+ session_store: spine.session_store, task_store: spine.task_store,
323
+ skill_store: skill_catalog.store, # the harvest's dedup reads the authored skills
324
+ tool_trace_store: executor_extra[:tool_trace_store],
325
+ settings_store: executor_extra[:settings_store],
326
+ negative_list: nil, miner_factory: nil,
327
+ event_stream: spine.event_stream
328
+ )
329
+ executor.harvest_engine = Insika::HarvestEngine.new(
330
+ store: spine.backend, harvest_store: spine.harvest_store,
331
+ session_store: spine.session_store, runner: run_harvest,
332
+ profiles: profiles, window: Insika::HarvestEngine::DEFAULT_WINDOW
333
+ )
155
334
  # WS6 operator alerts: answers budget_warning / breaker_open /
156
335
  # delivery_failed per agent (`alerts.webhook`) via the outbox+claim
157
336
  # pipeline. Always wired — the per-agent data gates it (parity).
@@ -166,8 +345,19 @@ module Insika
166
345
  session_store: spine.session_store, task_store: spine.task_store,
167
346
  checkpoint_store: spine.checkpoint_store, pending_action_store: spine.pending_action_store,
168
347
  delegation_store: spine.delegation_store,
169
- memory_store: spine.memory_store, refinement_store: spine.refinement_store,
170
- outbox_store: spine.outbox_store, inbound_log: spine.inbound_log,
348
+ memory_store: spine.memory_store, memory_audit_store: spine.memory_audit_store,
349
+ refinement_store: spine.refinement_store,
350
+ harvest_store: spine.harvest_store,
351
+ artifact_store: spine.artifact_store,
352
+ outbox_store: spine.outbox_store, shadow_pair_store: spine.shadow_pair_store,
353
+ inbound_log: spine.inbound_log,
354
+ outcome_store: spine.outcome_store,
355
+ funnel_store: spine.funnel_store,
356
+ contact_store: spine.contact_store,
357
+ followup_store: spine.followup_store,
358
+ schedule_store: spine.schedule_store,
359
+ proposal_store: spine.proposal_store,
360
+ knowledge_store: spine.knowledge_store,
171
361
  token_store: spine.token_store, budget_ledger: spine.budget_ledger,
172
362
  circuit_state: spine.circuit_state,
173
363
  channel_registry: spine.channel_registry, channel_delivery: channel_delivery,
@@ -181,10 +371,54 @@ module Insika
181
371
  )
182
372
  end
183
373
 
374
+ # The boot step behind every root's `load_plugins` (Server::Boot's first
375
+ # named step). Discovers plugin manifests, validates them, requires the
376
+ # entries and registers their contributions into the ALREADY-BUILT graph —
377
+ # which is safe by construction: every seam the Loader touches resolves at
378
+ # turn time (registries and hooks by name, the middleware stack and the
379
+ # provider list per call, the catalogs on reload), and the step runs
380
+ # single-fiber before the server accepts connections.
381
+ #
382
+ # Roots, highest precedence first (duplicate id -> first root wins):
383
+ # 1. workspace — INSIKA_PLUGIN_DIR (the operator's override spot);
384
+ # 2. gems — whatever called Insika::Plugin.announce (default-enabled);
385
+ # 3. bundled — `bundled_root` (the repo's plugins/; requires enabling).
386
+ # INSIKA_PLUGINS enables workspace/bundled ids; INSIKA_PLUGINS_DISABLED is
387
+ # the absolute veto (deny wins, like every allowlist in the engine).
388
+ def load_plugins(graph, env: ENV, bundled_root: nil)
389
+ announced = Insika::Plugin.announced_roots
390
+ workspace = Insika::EnvSchema.read("INSIKA_PLUGIN_DIR", env)
391
+ roots = ([workspace] + announced + [bundled_root]).compact.map { |d| File.expand_path(d.to_s) }
392
+
393
+ result = Insika::Plugin::Loader.new(
394
+ roots: roots,
395
+ registries: {
396
+ tools: graph.code_tool_registry, workflows: graph.workflow_registry,
397
+ policies: graph.policy_registry, capabilities: graph.capability_registry,
398
+ channels: graph.channel_registry, hooks: graph.hooks,
399
+ middleware: graph.middleware, context_providers: graph.context_providers
400
+ },
401
+ enabled: plugin_csv(Insika::EnvSchema.read("INSIKA_PLUGINS", env)),
402
+ disabled: plugin_csv(Insika::EnvSchema.read("INSIKA_PLUGINS_DISABLED", env)),
403
+ announced_roots: announced, event_stream: graph.event_stream
404
+ ).load_all
405
+
406
+ # Plugin knowledge joins at the LOWEST precedence: an operator's
407
+ # workspace/authored skill always beats a same-named plugin one.
408
+ graph.skill_catalog.add_roots(result[:skill_dirs])
409
+ graph.prompt_catalog.add_roots(result[:prompt_dirs])
410
+ result
411
+ end
412
+
413
+ def plugin_csv(value)
414
+ value.to_s.split(",").map(&:strip).reject(&:empty?)
415
+ end
416
+
184
417
  # The CORE command surface every root needs — turn essentials + the operator
185
418
  # controls (pause/approve) the Studio dispatches. Registering pause_task/
186
419
  # approve_action HERE is the crux of: it retires the config.ru:28-34 patch.
187
- def build_core_bus(spine:, profiles:, executor:)
420
+ def build_core_bus(spine:, profiles:, executor:, executor_extra: {}, skill_catalog: nil,
421
+ run_distillation: nil)
188
422
  bus = Insika::CommandBus.new
189
423
  bus.register(:create_session,
190
424
  Insika::Commands::CreateSession.new(session_store: spine.session_store, event_stream: spine.event_stream))
@@ -198,7 +432,10 @@ module Insika
198
432
  bus.register(:send_message,
199
433
  Insika::Commands::SendMessage.new(profiles: profiles, session_store: spine.session_store,
200
434
  task_store: spine.task_store, executor: executor,
201
- inbound_log: spine.inbound_log))
435
+ inbound_log: spine.inbound_log,
436
+ contact_store: spine.contact_store,
437
+ followup_store: spine.followup_store,
438
+ store: spine.backend))
202
439
  bus.register(:resume_task,
203
440
  Insika::Commands::ResumeTask.new(profiles: profiles, task_store: spine.task_store,
204
441
  checkpoint_store: spine.checkpoint_store, executor: executor))
@@ -214,6 +451,160 @@ module Insika
214
451
  bus.register(:rotate_tenant_token,
215
452
  Insika::Commands::RotateTenantToken.new(token_store: spine.token_store,
216
453
  event_stream: spine.event_stream))
454
+ # WS7: a business outcome per conversation (operator or integration).
455
+ bus.register(:record_outcome,
456
+ Insika::Commands::RecordOutcome.new(outcome_store: spine.outcome_store,
457
+ event_stream: spine.event_stream))
458
+ # the incumbent's half of a shadow pair, one command behind
459
+ # both mirror shapes (the mirror call itself + the follow-up route).
460
+ bus.register(:record_shadow_reply,
461
+ Insika::Commands::RecordShadowReply.new(shadow_pairs: spine.shadow_pair_store,
462
+ event_stream: spine.event_stream))
463
+ # WS8 (LGPD): purge one customer's memory + the whole footprint of their
464
+ # sessions (traces, tasks, checkpoints, outbox). The trace stores are
465
+ # deployment components (nil at the base — skipped). the
466
+ # audit collaborator records a content-free purge line.
467
+ bus.register(:forget_customer,
468
+ Insika::Commands::ForgetCustomer.new(
469
+ memory_store: spine.memory_store, session_store: spine.session_store,
470
+ tool_trace_store: executor_extra[:tool_trace_store],
471
+ context_trace_store: executor_extra[:context_trace_store],
472
+ # the model-visible traces die with the
473
+ # checkpoints (the same SessionPurge list).
474
+ model_visible_trace_store: Insika::ModelVisibleTraceStore.new(store: spine.backend),
475
+ task_store: spine.task_store, checkpoint_store: spine.checkpoint_store,
476
+ outbox_store: spine.outbox_store,
477
+ shadow_pairs: spine.shadow_pair_store,
478
+ audit_store: spine.memory_audit_store,
479
+ followup_store: spine.followup_store,
480
+ contact_store: spine.contact_store,
481
+ proposal_store: spine.proposal_store,
482
+ event_stream: spine.event_stream
483
+ ))
484
+ # the LGPD access right — export one customer's memory
485
+ # cell as content (the Studio download); the event stays counts-only.
486
+ bus.register(:export_customer_memory,
487
+ Insika::Commands::ExportCustomerMemory.new(
488
+ memory_store: spine.memory_store, event_stream: spine.event_stream
489
+ ))
490
+ # WS8 (LGPD): purge ONE TENANT's data — sessions and their footprint,
491
+ # memory cells and outcomes. Operator-only by construction (ingress).
492
+ bus.register(:delete_tenant_data,
493
+ Insika::Commands::DeleteTenantData.new(
494
+ memory_store: spine.memory_store, session_store: spine.session_store,
495
+ tool_trace_store: executor_extra[:tool_trace_store],
496
+ context_trace_store: executor_extra[:context_trace_store],
497
+ # the model-visible traces die with the tenant.
498
+ model_visible_trace_store: Insika::ModelVisibleTraceStore.new(store: spine.backend),
499
+ outcome_store: spine.outcome_store,
500
+ funnel_store: spine.funnel_store, # the fold dies with the tenant
501
+ followup_store: spine.followup_store,
502
+ contact_store: spine.contact_store,
503
+ proposal_store: spine.proposal_store,
504
+ harvest_store: spine.harvest_store,
505
+ schedule_store: spine.schedule_store,
506
+ artifact_store: spine.artifact_store,
507
+ task_store: spine.task_store, checkpoint_store: spine.checkpoint_store,
508
+ outbox_store: spine.outbox_store,
509
+ shadow_pairs: spine.shadow_pair_store,
510
+ token_store: spine.token_store, # revoked BEFORE the sweep
511
+ event_stream: spine.event_stream
512
+ ))
513
+ # the operator's baseline freeze — the number
514
+ # read. Synchronous control command, dispatched from the Studio.
515
+ bus.register(:freeze_funnel_baseline,
516
+ Insika::Commands::FreezeFunnelBaseline.new(
517
+ funnel_store: spine.funnel_store, profiles: profiles,
518
+ event_stream: spine.event_stream
519
+ ))
520
+ # the follow-up mutations — the Studio's Cancel button
521
+ # and the channel opt-out event (a tenant principal never reaches the
522
+ # generic command ingress; these ride the same operator-grade path).
523
+ bus.register(:cancel_followup,
524
+ Insika::Commands::CancelFollowup.new(followup_store: spine.followup_store,
525
+ event_stream: spine.event_stream))
526
+ # the report destination's Studio mutation — delete one
527
+ # artifact (the only write on the Artifacts tab; a bus command, like
528
+ # every Studio mutation).
529
+ bus.register(:delete_artifact,
530
+ Insika::Commands::DeleteArtifact.new(artifact_store: spine.artifact_store,
531
+ event_stream: spine.event_stream))
532
+ bus.register(:revoke_contact,
533
+ Insika::Commands::RevokeContact.new(contact_store: spine.contact_store,
534
+ followup_store: spine.followup_store,
535
+ store: spine.backend,
536
+ event_stream: spine.event_stream))
537
+ # the human's answer on a distilled proposal — the ONLY
538
+ # mutation behind the Facts (wiki) page. Synchronous control command.
539
+ bus.register(:resolve_proposal,
540
+ Insika::Commands::ResolveProposal.new(
541
+ proposal_store: spine.proposal_store,
542
+ memory_store: spine.memory_store,
543
+ event_stream: spine.event_stream
544
+ ))
545
+ # /C10: the gated harvest — the mining pass, the double
546
+ # gate, the human's promote/rollback/dismiss. The BASE graph wires the
547
+ # nil factories (no eval surface, no criterion, no funnel): every flow
548
+ # refuses with a named reason or skips (parity). The deployment root
549
+ # re-registers with the real gate/criterion/negative list.
550
+ bus.register(:run_harvest,
551
+ Insika::Commands::RunHarvest.new(
552
+ profiles: profiles, harvest_store: spine.harvest_store,
553
+ session_store: spine.session_store, task_store: spine.task_store,
554
+ skill_store: skill_catalog.store,
555
+ tool_trace_store: executor_extra[:tool_trace_store],
556
+ settings_store: executor_extra[:settings_store],
557
+ negative_list: nil, miner_factory: nil,
558
+ event_stream: spine.event_stream
559
+ ))
560
+ bus.register(:gate_harvest,
561
+ Insika::Commands::GateHarvest.new(
562
+ harvest_store: spine.harvest_store, gate: nil,
563
+ conversion_gate: nil, criterion: nil,
564
+ event_stream: spine.event_stream
565
+ ))
566
+ bus.register(:promote_harvest,
567
+ Insika::Commands::PromoteHarvest.new(
568
+ harvest_store: spine.harvest_store,
569
+ skill_store: skill_catalog.store,
570
+ skill_catalog: skill_catalog,
571
+ profile_source: profiles, criterion: nil, conversion_gate: nil,
572
+ event_stream: spine.event_stream
573
+ ))
574
+ bus.register(:rollback_harvest,
575
+ Insika::Commands::RollbackHarvest.new(
576
+ harvest_store: spine.harvest_store,
577
+ skill_store: skill_catalog.store,
578
+ skill_catalog: skill_catalog,
579
+ profile_source: profiles, event_stream: spine.event_stream
580
+ ))
581
+ bus.register(:reject_harvest,
582
+ Insika::Commands::RejectHarvest.new(
583
+ harvest_store: spine.harvest_store, event_stream: spine.event_stream
584
+ ))
585
+ # the ONE path that writes proposals, served to the
586
+ # Studio's "Run distillation now" button. The same instance the
587
+ # DistillEngine's loop uses, so the manual and the automatic share one
588
+ # code path; the engine keeps the automatic pass when the root does
589
+ # not pass a runner here (nil = the command is not on the bus).
590
+ bus.register(:run_distillation, run_distillation) if run_distillation
591
+ # the demo seed's ONE path — the CLI's `insika demo:seed` builds
592
+ # the same Insika::Demo::Seeder itself (no executor, no bus, from
593
+ # Graph.spine alone), and the Studio's "Seed demo data" button
594
+ # dispatches this bus command; nothing else writes the demo agent.
595
+ config_store = Insika::ConfigStore.new(store: spine.backend)
596
+ demo_seeder = Insika::Demo::Seeder.new(
597
+ profiles: profiles, store: spine.backend, session_store: spine.session_store,
598
+ task_store: spine.task_store, outcome_store: spine.outcome_store,
599
+ funnel_store: spine.funnel_store, followup_store: spine.followup_store,
600
+ refinement_store: spine.refinement_store, pending_action_store: spine.pending_action_store,
601
+ proposal_store: spine.proposal_store, memory_store: spine.memory_store,
602
+ golden_store: Insika::GoldenStore.new(config_store: config_store),
603
+ baseline_store: Insika::BaselineStore.new(config_store: config_store),
604
+ event_stream: spine.event_stream
605
+ )
606
+ bus.register(:seed_demo_data,
607
+ Insika::Commands::SeedDemoData.new(seeder: demo_seeder, event_stream: spine.event_stream))
217
608
  bus
218
609
  end
219
610
 
@@ -221,9 +612,15 @@ module Insika
221
612
  # them to their historic public constants (SESSION_STORE, REGISTRY, ...).
222
613
  Spine = Struct.new(
223
614
  :backend, :event_stream, :session_store, :task_store, :checkpoint_store,
224
- :pending_action_store, :delegation_store, :memory_store, :refinement_store,
225
- :token_store, :budget_ledger, :circuit_state, :outbox_store, :inbound_log, :code_tool_registry,
226
- :workflow_registry, :policy_registry, :capability_registry, :hooks,
615
+ :pending_action_store, :delegation_store, :memory_store, :memory_audit_store,
616
+ :refinement_store,
617
+ :harvest_store,
618
+ :token_store, :budget_ledger, :circuit_state, :outbox_store, :shadow_pair_store,
619
+ :inbound_log, :outcome_store, :funnel_store,
620
+ :contact_store, :followup_store, :schedule_store, :proposal_store,
621
+ :knowledge_store,
622
+ :artifact_store,
623
+ :code_tool_registry, :workflow_registry, :policy_registry, :capability_registry, :hooks,
227
624
  :channel_registry, keyword_init: true
228
625
  )
229
626
 
@@ -233,8 +630,15 @@ module Insika
233
630
  Result = Struct.new(
234
631
  :backend, :event_stream,
235
632
  :session_store, :task_store, :checkpoint_store, :pending_action_store, :delegation_store,
236
- :memory_store, :refinement_store, :outbox_store, :inbound_log, :token_store,
237
- :budget_ledger, :circuit_state, :channel_registry, :channel_delivery,
633
+ :memory_store, :memory_audit_store, :refinement_store, :outbox_store, :shadow_pair_store,
634
+ :inbound_log, :token_store,
635
+ :budget_ledger, :circuit_state, :outcome_store, :funnel_store,
636
+ :contact_store, :followup_store, :proposal_store,
637
+ :schedule_store,
638
+ :harvest_store,
639
+ :knowledge_store,
640
+ :artifact_store,
641
+ :channel_registry, :channel_delivery,
238
642
  :code_tool_registry, :tool_registry, :workflow_registry, :policy_registry, :capability_registry,
239
643
  :tool_catalog, :skill_catalog, :prompt_catalog,
240
644
  :hooks, :guardrails, :middleware,
@@ -244,6 +648,61 @@ module Insika
244
648
  ) do
245
649
  def durable? = backend.is_a?(Insika::Stores::SQLite)
246
650
  end
651
+
652
+ # The save_artifact tool, registered OPTIONAL on the shared code registry
653
+ # (the per-agent allowlist is the switch). The signing/base config is
654
+ # bound at registration time from the environment — the tool instance is
655
+ # per-deployment, and the tenant/agent/task bindings arrive per-turn via
656
+ # the deposited turn context.
657
+ # `run_persona_eval` (C3.1): a QA agent's own probe against a SIBLING
658
+ # agent of THIS SAME graph. Registered OPTIONAL on the shared code
659
+ # registry, same switch as `save_artifact` — but unlike that one, this
660
+ # is NOT called unconditionally from `build`: it needs a `golden_store`
661
+ # and a `settings_store` (the persona model + judge panel), and neither
662
+ # lives on the spine (the base/minimal wiring builds without either, the
663
+ # same way it builds without a Studio) — so each root calls this itself,
664
+ # once it has both. `config/deployment.rb` (round1, and the actual
665
+ # production `config.ru` root) and `DSL::Runtime` both do.
666
+ #
667
+ # `graph` is the ALREADY-RETURNED `Graph::Result` — bus/executor fully
668
+ # assembled by the time a caller has it to pass here, so `GraphChat.new`
669
+ # needs no laziness of its own (the tool's own factory block is what is
670
+ # lazy — this method just registers it).
671
+ # `llm:` — the caller's own RubyLLM::Context, when it has one
672
+ # (DSL::Runtime does; config/deployment.rb does not, and reads the
673
+ # process-wide RubyLLM constant like the rest of that root already
674
+ # does). Forwarded as-is so the tool's OWN persona/judge calls spend
675
+ # the right credentials instead of silently defaulting to nil.
676
+ def register_persona_eval_tool(graph, golden_store:, settings_store:, llm: nil)
677
+ graph.code_tool_registry.register("run_persona_eval", optional: true) do
678
+ require "ruby_llm"
679
+ require_relative "../tools/run_persona_eval"
680
+ Insika::Tools::RunPersonaEval.new(
681
+ golden_store: golden_store, profiles: graph.profiles, tool_registry: graph.tool_registry,
682
+ runtime: GraphChat.new(graph: graph), graph: graph, settings_store: settings_store,
683
+ budget_ledger: graph.budget_ledger, event_stream: graph.event_stream, llm: llm
684
+ )
685
+ end
686
+ end
687
+
688
+ def register_artifact_tool(spine)
689
+ signing_key = Insika::EnvSchema.read("INSIKA_ARTIFACT_SIGNING_KEY")
690
+ signing_ttl = Insika::EnvSchema.read("INSIKA_ARTIFACT_SIGNING_TTL")
691
+ max_bytes = Insika::EnvSchema.read("INSIKA_ARTIFACT_MAX_BYTES")
692
+ base_url = Insika::Coercion.presence(Insika::EnvSchema.read("INSIKA_PUBLIC_URL"))
693
+ spine.code_tool_registry.register("save_artifact", optional: true) do
694
+ require "ruby_llm"
695
+ require_relative "../tools/save_artifact"
696
+ Insika::Tools::SaveArtifact.new(
697
+ artifact_store: spine.artifact_store,
698
+ base_url: base_url,
699
+ signing_key: signing_key,
700
+ signing_ttl: signing_ttl&.to_i,
701
+ max_bytes: max_bytes&.to_i,
702
+ event_stream: spine.event_stream
703
+ )
704
+ end
705
+ end
247
706
  end
248
707
  end
249
708
  end