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
data/docs/ARTIFACTS.md ADDED
@@ -0,0 +1,95 @@
1
+ ---
2
+ title: Artifacts
3
+ parent: Operate
4
+ nav_order: 3
5
+ permalink: /artifacts/
6
+ ---
7
+
8
+ # Artifacts — a report the agent can hand you a URL to
9
+
10
+ A channel message is ephemeral, unformatted and capped. A scheduled report turn
11
+ (see [Schedules](SCHEDULING.md)) produces something no message can carry: a page
12
+ — tables, sections, inline charts. An **artifact** is that page as a thing that
13
+ exists afterwards: listable, linkable, and — because it carries customer PII —
14
+ deletable on the same terms as everything else the engine stores.
15
+
16
+ Deliberately small: a store, a tool, a route. Not a CMS.
17
+
18
+ ## The `save_artifact` tool
19
+
20
+ A registry tool, allowlisted per agent like every tool — **the allowlist IS the
21
+ switch**:
22
+
23
+ ```ruby
24
+ agent = Insika.agent("reporter") do
25
+ instructions "…"
26
+ tools_allow %w[save_artifact] # without this, the tool is not even offered
27
+ end
28
+ ```
29
+
30
+ The agent hands in `title` + `content` (default mime `text/html`; also
31
+ `text/markdown` and `image/svg+xml`) and gets the URL back, which it can include
32
+ in a channel message ("today's report: <url>"). When a signing key is
33
+ configured (below), the result also carries a `signed_url` that expires.
34
+
35
+ ```jsonc
36
+ { "id": "…", "url": "https://…/studio/artifacts/<id>/content",
37
+ "signed_url": "https://…/studio/artifacts/s/<id>?exp=…&sig=…" } // only with a key
38
+ ```
39
+
40
+ The tenant binding is **inherited, never chosen**: an artifact belongs to the
41
+ tenant of the agent that saved it — a binding of the tool instance, never a
42
+ parameter the model types. Store A's report can never appear in, or be linked
43
+ from, store B.
44
+
45
+ ## Serving
46
+
47
+ - `GET /studio/artifacts` — the Studio's per-agent list (the listing IS the
48
+ history; no versioning — one report per run).
49
+ - `GET /studio/artifacts/:id` — the preview page, rendered **inside a sandboxed
50
+ iframe** (no scripts, no same-origin, no forms).
51
+ - `GET /studio/artifacts/:id/content` — the raw page (authenticated).
52
+ - `GET /studio/artifacts/s/:id?exp=…&sig=…` — the **signed link**: the only
53
+ artifact route that works without a Studio session. HMAC-SHA256 over
54
+ `(id, expiry)` with `INSIKA_ARTIFACT_SIGNING_KEY`, verified in constant time.
55
+ Expired or bad signatures **404 (never 403 — no oracle)**. Rotating the key
56
+ invalidates every outstanding link — the documented behavior, not a bug.
57
+ Without `INSIKA_ARTIFACT_SIGNING_KEY` there is no signed surface at all.
58
+
59
+ **Artifact content is untrusted.** It is LLM output. Both content routes send:
60
+
61
+ ```http
62
+ Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data:
63
+ X-Content-Type-Options: nosniff
64
+ ```
65
+
66
+ No script, no external fetch, no forms. The model writes HTML with **inline
67
+ SVG** for charts — that is a skill instruction (palette, tables, pure-SVG bars),
68
+ not engine code. A "real" charting need is a plugin.
69
+
70
+ ## Limits and retention
71
+
72
+ - **Size cap** — `INSIKA_ARTIFACT_MAX_BYTES` (default 1 MB): an artifact is a
73
+ page, not an attachment. The mime allowlist is `text/html`, `text/markdown`,
74
+ `image/svg+xml`; no binaries, no uploads.
75
+ - **Expiry** — the settings key `artifact_ttl_days` (Integer days; absent = OFF)
76
+ ages artifacts out on the retention sweep's own daily pass, **independent of
77
+ `retention_days`**: a deployment that keeps its conversations forever must
78
+ still expire the reports. This is the guarantee that PII inside a report
79
+ expires — the honest reach, because no reader can see inside the opaque HTML.
80
+
81
+ ## Privacy
82
+
83
+ - `delete_tenant_data` deletes the tenant's artifacts (the tenant binding is
84
+ the isolation boundary).
85
+ - `forget_customer` **cannot** know which artifacts mention a customer (content
86
+ is opaque HTML), so per-customer redaction inside a report is not pretended to
87
+ exist; the `artifact_ttl_days` knob is the guarantee that a report's PII
88
+ expires.
89
+
90
+ ## See also
91
+
92
+ - [Schedules](SCHEDULING.md) — the recurring turns whose output lands here.
93
+ - [Tools](TOOLS.md) — how a tool enters the per-agent allowlist.
94
+ - [`examples/scheduled-report/`](https://github.com/guizaols/insika/tree/main/examples/scheduled-report/)
95
+ — schedule + skill + data tool + artifact, tenant-bound, end to end.
data/docs/BENCHMARK.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Benchmark
3
- parent: Operate & prove it
4
- nav_order: 2
3
+ parent: Operate
4
+ nav_order: 6
5
5
  permalink: /benchmark/
6
6
  ---
7
7
 
data/docs/CHANNELS.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Channels
3
- parent: Build an agent
4
- nav_order: 6
3
+ parent: Integrate
4
+ nav_order: 2
5
5
  permalink: /channels/
6
6
  ---
7
7
 
@@ -176,21 +176,24 @@ again.
176
176
  ## Relay or the drop-in API?
177
177
 
178
178
  If you already own your messaging platform, you can reach the engine two ways: the
179
- drop-in [`POST /v1/responses`](/architecture/) — you hold an SSE connection for the
179
+ drop-in [`POST /v1/responses`](ARCHITECTURE.md) — you hold an SSE connection for the
180
180
  whole turn and read the answer off it — or the relay, where the engine acks in
181
181
  milliseconds and POSTs the answer to you when it exists.
182
182
 
183
183
  The instinct is that streaming gets the customer their reply sooner, and that the
184
- relay trades that away. **It does not, and the reason is structural:** the engine
185
- publishes `:content` as the ANSWER, whole, after the turn's hooks
186
- ([what crosses the edge](/architecture/#what-crosses-the-edge)). During the turn the
187
- stream carries tool activity; the text arrives in one piece at the end. Measured on
188
- a real store agent, the text frames span **0 ms** — there is nothing to deliver
189
- progressively, on either path.
184
+ relay trades that away. **For the default relay it does not, and the reason is
185
+ structural:** the engine publishes `:content` as the ANSWER, whole, after the
186
+ turn's hooks ([what crosses the edge](ARCHITECTURE.md#what-crosses-the-edge)).
187
+ During the turn the stream carries tool activity; the text arrives in one piece at
188
+ the end. Measured on a real store agent, the text frames span **0 ms** — there is
189
+ nothing to deliver progressively. That is the `:at_end` fact, true of `/v1/responses`
190
+ and of a relay that never opted into [progressive delivery](#delivery-policy).
191
+ The relay can opt out of it per channel — the opt-in is below, and it is the one
192
+ thing that changes the 0 ms span.
190
193
 
191
194
  | | drop-in `/v1/responses` | relay |
192
195
  |---|---|---|
193
- | What the customer receives | one message, at the end | one message, at the end |
196
+ | What the customer receives | one message, at the end | one message at the end (`:at_end`), or one WhatsApp balloon per paragraph with the first one as soon as the answer exists (`delivery: :progressive`) |
194
197
  | Your app's request | held open for the whole turn (seconds) | acked in **milliseconds** |
195
198
  | A turn that outlives your HTTP timeout | your problem | already handled — the answer arrives later |
196
199
  | Retry on a failed handover | yours to build | the engine's outbox, bounded, at-most-once |
@@ -201,7 +204,7 @@ That last row is the one that cannot be had the other way. `/v1/responses` answe
201
204
  the request it was given, so a message that arrives while a turn is running is a
202
205
  second turn — the engine has no way to tell you "this joined the previous one". The
203
206
  relay's `merged` / `steered` acks exist precisely to say that, which is why
204
- [the inbound queue](/agents/#queue_mode--when-a-message-arrives-while-the-agent-is-busy)
207
+ [the inbound queue](AGENTS.md#queue_mode--when-a-message-arrives-while-the-agent-is-busy)
205
208
  is only reachable from here.
206
209
 
207
210
  **Measured, so you can judge it rather than take our word:** same agent, same
@@ -279,18 +282,20 @@ answers, and **they are four different facts**:
279
282
  answer two or three times.** That is the one contract mistake that is visible to
280
283
  the end user, so it is worth a line of code: only deliver for a `202`.
281
284
 
282
- `merged` and `steered` come from the [inbound queue](/agents/#queue_mode--when-a-message-arrives-while-the-agent-is-busy)
285
+ `merged` and `steered` come from the [inbound queue](AGENTS.md#queue_mode--when-a-message-arrives-while-the-agent-is-busy)
283
286
  (`limits[:queue_mode]`). They only ever occur if you turned that on for the agent;
284
287
  with the default `followup` you will only see `202` and `duplicate`.
285
288
 
286
289
  ### Outbound
287
290
 
288
- One POST per reply, to the URL you configured:
291
+ One POST per **balloon**, to the URL you configured. For the default `:at_end`
292
+ delivery a turn emits exactly one; a progressive turn may emit several — `task_id`
293
+ is the correlation, `index` is the order.
289
294
 
290
295
  ```jsonc
291
296
  POST <INSIKA_RELAY_DELIVER_URL>
292
297
  Authorization: Bearer <INSIKA_RELAY_DELIVER_TOKEN> // omitted if unset
293
- X-Insika-Delivery: 0f2c… // stable idempotency key
298
+ X-Insika-Delivery: 0f2c… // stable idempotency key, PER balloon
294
299
  Content-Type: application/json
295
300
 
296
301
  {
@@ -307,9 +312,49 @@ short backoff, and then the delivery is marked `failed` and stops.
307
312
  `content` is the turn's **answer** — one message, whole. The model's narration on
308
313
  the way to an answer ("vou verificar o cardápio…") does not come through here; it
309
314
  stays internal unless the agent opts in. That contract is
310
- [the edge contract](/architecture/#what-crosses-the-edge), and it is why you can
315
+ [the edge contract](ARCHITECTURE.md#what-crosses-the-edge), and it is why you can
311
316
  forward `content` straight to the customer.
312
317
 
318
+ ### Delivery policy
319
+
320
+ A relay declares **how the outbox flushes** — a property of the surface
321
+ (WhatsApp balloons), not of the model:
322
+
323
+ - **`:at_end`** (the default, also `INSIKA_RELAY_DELIVERY=at_end` or unset): one
324
+ POST at the end with the whole answer. Byte-identical to the contract above.
325
+ - **`:progressive`** (`INSIKA_RELAY_DELIVERY=progressive`): the answer is split
326
+ into balloons at paragraph boundaries (a soft 600-char cap splits a single long
327
+ paragraph on sentences; fenced code blocks are atomic), and each balloon is its
328
+ own POST, in order, starting as soon as the answer exists. The consumer sends
329
+ each POST as its own platform message.
330
+
331
+ A progressive POST carries two additive fields **only when the turn split into
332
+ more than one balloon** — a one-balloon progressive turn is indistinguishable
333
+ from `:at_end` on the wire:
334
+
335
+ ```jsonc
336
+ {
337
+ "external_id": "5511999998888",
338
+ "session_id": "relay:5511999998888",
339
+ "task_id": "…",
340
+ "content": "Seu pedido saiu para entrega hoje",
341
+ "index": 0, // this balloon's position, 0-based
342
+ "final": false // true on the LAST balloon of this task_id
343
+ }
344
+ ```
345
+
346
+ When `index`/`final` are present, this POST is **one balloon of several for the
347
+ same `task_id`** — forward `content` as its own platform message and honor
348
+ `X-Insika-Delivery` per balloon. If you only forward `content` and ignore the new
349
+ fields, progressive turns still read as N messages in arrival order (which is
350
+ index order — the engine dispatches the chain sequentially); single-balloon turns
351
+ behave exactly as before. A consumer that ignores unknown keys keeps working.
352
+
353
+ The engine measures the win in-process: every channel turn records
354
+ `first_balloon_ms` (inbound receipt → first outbox flush) on the task record and
355
+ the terminal event, so the Studio task page shows whether the 2 s target is being
356
+ hit without toggling any flag.
357
+
313
358
  ### Deduplication
314
359
 
315
360
  Send `event_id` and a retried webhook costs you nothing: the engine recognizes the
@@ -334,7 +379,7 @@ your side: you already have the customer's conversation, and `GET /v1/tasks/:id`
334
379
  tells you the turn's terminal state.
335
380
 
336
381
  A turn that **failed** delivers nothing — an error string is not an answer. Watch
337
- `GET /v1/tasks/:id` or the [event stream](/observability/) for those.
382
+ `GET /v1/tasks/:id` or the [event stream](OBSERVABILITY.md) for those.
338
383
 
339
384
  ## Setting up the relay
340
385
 
@@ -344,6 +389,7 @@ Three environment variables on the engine:
344
389
  INSIKA_RELAY_TOKEN=<a long random secret> # the switch AND the credential
345
390
  INSIKA_RELAY_DELIVER_URL=https://you.example/insika/deliver
346
391
  INSIKA_RELAY_DELIVER_TOKEN=<another secret> # optional; what we send to you
392
+ INSIKA_RELAY_DELIVERY=progressive # optional; "at_end" (the default) = one POST
347
393
  ```
348
394
 
349
395
  `INSIKA_RELAY_TOKEN` is the switch: without it the channel is not mounted and
@@ -351,7 +397,7 @@ INSIKA_RELAY_DELIVER_TOKEN=<another secret> # optional; what we send to you
351
397
  without a credential — a public inbound route with an LLM behind it is a money
352
398
  faucet, so it fails closed by construction.
353
399
 
354
- The delivery POST goes through the same [egress guard](/security/#egress-the-ssrf-boundary) as
400
+ The delivery POST goes through the same [egress guard](SECURITY.md#egress-the-ssrf-boundary) as
355
401
  data-tools: **https only**, and private/loopback destinations blocked. For local
356
402
  development, where your consumer is on `localhost`:
357
403
 
@@ -367,6 +413,50 @@ nothing at all).
367
413
  A runnable consumer in ~40 lines lives in
368
414
  [`examples/relay-channel/`](https://github.com/guizaols/insika/tree/main/examples/relay-channel).
369
415
 
416
+ ## Shadow mode
417
+
418
+ Shadow mode lets one channel run every turn **end to end and deliver
419
+ nothing** — the experiment that answers "can we replace the incumbent?" before
420
+ any customer is handed over. The incumbent keeps answering; the engine records
421
+ what it *would* have answered, and the two replies are judged pairwise against a
422
+ **frozen criterion** — the file `INSIKA_PARITY_CRITERION` points at.
423
+
424
+ ```bash
425
+ INSIKA_RELAY_SHADOW=1 # the switch
426
+ # INSIKA_PARITY_CRITERION — required in shadow mode
427
+ ```
428
+
429
+ Three things change when it is on:
430
+
431
+ - The turn still runs; the reply is recorded as a **pair** and never reaches the
432
+ customer. Zero outbox records, ever — and `Relay#deliver` refuses loudly if one
433
+ somehow exists.
434
+ - The inbound ack becomes `200 {"task_id": …, "shadow": true}` instead of `202`,
435
+ so a consumer wired to "202 means a reply is coming" cannot be misled.
436
+ - `event_id` becomes **required** — it is the correlation key both halves of the
437
+ pair are built from.
438
+
439
+ The incumbent's reply enters the same pair through one of two shapes: alongside
440
+ the mirror call itself (`"incumbent_reply": "…"` on `POST /channels/relay/events`),
441
+ or as a follow-up when the consumer answers first:
442
+
443
+ ```jsonc
444
+ POST /channels/relay/shadow-reply
445
+ Authorization: Bearer <INSIKA_RELAY_TOKEN>
446
+ { "external_id": "5511999998888", "event_id": "wamid.HBg…",
447
+ "reply": "Claro! Me passa o número do pedido?", "at": "2026-…Z" }
448
+ → 202 { "pair_id": "9f2c…", "status": "open" }
449
+ ```
450
+
451
+ Both shapes land in one command; a retried reply is ignored (first write wins —
452
+ the customer received one reply, and a retry must not rewrite evidence).
453
+
454
+ **No criterion, no shadow.** Boot refuses when shadow is on and
455
+ the criterion file is missing or unparseable — a number nobody pre-registered
456
+ does not count. The Studio's Parity page folds the running verdict on demand
457
+ from the pair store; `insika doctor` reports the shadow configuration before
458
+ boot does.
459
+
370
460
  ## Sessions
371
461
 
372
462
  The engine mints the session id: `relay:<your external_id>`. Namespacing is not
@@ -380,7 +470,7 @@ redirect its own conversation.
380
470
  ## Writing your own channel
381
471
 
382
472
  A channel is a plain object — no base class. Register it from a plugin
383
- (see [Plugins](/plugins/)) with `contracts: { channels: [<id>] }` in the manifest,
473
+ (see [Plugins](PLUGINS.md)) with `contracts: { channels: [<id>] }` in the manifest,
384
474
  and it mounts under `/channels/<id>/`.
385
475
 
386
476
  Two members are always there; the rest of the object decides which shape you get.
@@ -446,8 +536,8 @@ command carries `transport: "channel:<id>"`.
446
536
 
447
537
  ## See also
448
538
 
449
- - [Security](/security/) — the tokens, the egress guard, and why the rate limit
539
+ - [Security](SECURITY.md) — the tokens, the egress guard, and why the rate limit
450
540
  matters for anything public.
451
- - [Agents](/agents/) — `limits[:queue_mode]`, which is what produces `merged` and
541
+ - [Agents](AGENTS.md) — `limits[:queue_mode]`, which is what produces `merged` and
452
542
  `steered`.
453
- - [Observability](/observability/) — the event stream and OpenTelemetry.
543
+ - [Observability](OBSERVABILITY.md) — the event stream and OpenTelemetry.
data/docs/CONTEXT.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Context
3
- parent: Build an agent
4
- nav_order: 4
3
+ parent: Core concepts
4
+ nav_order: 5
5
5
  permalink: /context/
6
6
  ---
7
7
 
@@ -20,26 +20,30 @@ Providers are chosen by a double gate (the provider opts in for the profile **an
20
20
  the agent's `context_providers` allowlist permits it), then assembled by priority
21
21
  into a deterministic prompt:
22
22
 
23
- | Provider | Block | Priority | Notes |
24
- |----------|-------|:--------:|-------|
25
- | **Identity** | system | **100 — pinned** | The agent's prompt files (global system files first). Never cut. |
26
- | **Skill trigger** | `<active_skill>` | 85 | Level-2 bodies: the agent's `skills_eager` set, plus the ones whose `triggers:` match the message — see [Skills](SKILLS.md). |
27
- | **Skills** | `<available_skills>` | 80 | Level-1 skill list, minus whatever is already eager — see [Skills](SKILLS.md). |
28
- | **Memory** | `<memory>` | 75 | Durable facts + recent notes, only if `memory` is on. Cuttable. |
29
- | **Tool search** | `<available_tools>` | 70 | Level-1 list of deferred tools — see [Tools](TOOLS.md). |
30
- | **Session** | history | 60–79 | The running transcript; priority scales with recency. |
31
- | **Request** | `<request_context>` | 40 | Turn variables + tenant. Most cuttable; sits last. |
32
-
33
- The ordering is deliberate: the **stable identity sits first**, the **volatile
34
- request context sits last**. That keeps the cacheable prefix byte-stable (see the
35
- prefix cache below).
23
+ | Provider | Block | Priority | Layer | Notes |
24
+ |----------|-------|:--------:|-------|-------|
25
+ | **Identity** | system | **100 — pinned** | identity | The agent's prompt files (global system files first). Never cut. |
26
+ | **Skills** | `<available_skills>` | 80 | identity | Level-1 skill list, minus whatever is already eager — see [Skills](SKILLS.md). |
27
+ | **Tool search** | `<available_tools>` | 70 | identity | Level-1 list of deferred tools — see [Tools](TOOLS.md). |
28
+ | **Skill trigger** | `<active_skill>` | 85 | volatile | Level-2 bodies: the agent's `skills_eager` set, plus the ones whose `triggers:` match the message — see [Skills](SKILLS.md). |
29
+ | **Knowledge** | `<knowledge>` | 77 | volatile | Level-1 top-K learned concepts for the turn's message (+ one-hop `[[links]]`), only if `knowledge.retrieve` is on. Cuttable — see [Knowledge](KNOWLEDGE.md). |
30
+ | **Memory** | `<memory>` | 75 | volatile | Durable facts + recent notes, only if `memory` is on. Cuttable. |
31
+ | **Briefing** | `<briefing>` | 65 | volatile | The session's working state (known fields, still-missing list, next step) — only if the pack declared `briefing_fields`. Cuttable. |
32
+ | **Session** | history | 60–79 | volatile | The running transcript; priority scales with recency. |
33
+ | **Request** | `<request_context>` | 40 | volatile | Turn variables + tenant. Most cuttable; sits last. |
34
+
35
+ The ordering is deliberate: the render order is **identity layer first, volatile
36
+ layer after** — nothing volatile can sit above the cache boundary, whatever its
37
+ priority — and within each layer the priority sort above holds. That keeps the
38
+ cacheable prefix byte-stable (see the prefix cache below).
36
39
 
37
40
  ## Budget and eviction — the actual "compaction"
38
41
 
39
42
  - The cap is `profile.limits[:context_budget]`, **default 8000 tokens**.
40
43
  - To fit the budget, the builder cuts **non-pinned** fragments
41
44
  lowest-priority-first (ties broken by oldest history first). Under pressure you
42
- lose old history, then the memory block, then request context **the pinned
45
+ lose request context first, then old history, then briefing, then memory, then
46
+ learned knowledge, then the skill/tool-search level-1 lists — **the pinned
43
47
  identity is never truncated**.
44
48
  - A **pinned** fragment (the identity) that *alone* exceeds the budget raises an
45
49
  error — the turn fails rather than shipping a truncated identity.
@@ -78,19 +82,82 @@ budget first.
78
82
  With `memory` enabled, an agent gains a built-in `remember` tool for durable
79
83
  facts, and those facts (plus recent notes) are injected back into the prompt on
80
84
  later turns — **including turns in a different session**. Memory is scoped per
81
- agent. This is distinct from *session history*, which is the transcript of one
85
+ agent, per `(tenant, customer)` when the message carries a `customer`, and per
86
+ session otherwise — a session's own memory lives in a marked `memory:chat:<session id>`
87
+ cell, never a bare one, so the Customers drill cannot read a conversation as a
88
+ customer. This is distinct from *session history*, which is the transcript of one
82
89
  conversation; memory is the small set of facts that should outlive any single
83
90
  conversation. Facts and notes are editable from the Studio agent page. See
84
91
  [`examples/memory/`](https://github.com/guizaols/insika/tree/main/examples/memory/) for a runnable cross-session example.
85
92
 
93
+ Facts carry **provenance metadata**: every fact record stores `origin`
94
+ (who wrote it — `"engine"`, `"operator"`, `"legacy"` or `"distilled"`),
95
+ `created_at` / `updated_at` timestamps, and an optional `expires_at` (ISO8601) —
96
+ **an expired fact is never injected**, even before the daily sweep prunes it. The
97
+ Studio Customers drill reads and edits the same cell the next turn reads (injection
98
+ unchanged), and every operator mutation lands in the content-free audit trail
99
+ (digests, never values). The sweep honors the `memory_ttl_days` setting on its own
100
+ knob — see [Security](SECURITY.md#memory-and-the-right-to-be-forgotten-lgpd).
101
+
102
+ An **approved distilled fact** (see [Facts](FACTS.md)) lands in the
103
+ same cell this provider injects, stamped `distilled:<session_ref>` — approved on
104
+ the Studio Facts page, never applied automatically.
105
+
106
+ ## Briefing — the session's working state
107
+
108
+ The **briefing** is the per-conversation working state the agent keeps and asks
109
+ for: which facts it already learned (size, budget, delivery day) and the agreed
110
+ next step. It is **engine-owned data** — one `"briefing"` key on the session
111
+ record, written only by the agent through tools — whose *fields* come from the
112
+ pack:
113
+
114
+ ```jsonc
115
+ // pack agent.config.json
116
+ { "id": "store-support", "briefing_fields": ["size", "budget", "delivery_day"] }
117
+ ```
118
+
119
+ ```ruby
120
+ # or the DSL — [] = the feature is off (no block, no tools)
121
+ briefing_fields "size", "budget", "delivery_day"
122
+ ```
123
+
124
+ With fields declared, the turn's `:system` context gains a `<briefing>` block
125
+ (priority 65 — below identity/skill/memory so it never breaks the cacheable
126
+ prefix, above the turn's own `<request_context>`):
127
+
128
+ ```
129
+ <briefing>
130
+ known:
131
+ size: M
132
+ still missing: budget, delivery_day
133
+ next step: send the payment link tomorrow at 10
134
+ </briefing>
135
+ ```
136
+
137
+ The `still missing` list is the point: the *model* sees which declared fields are
138
+ still unanswered, so it stops re-asking for something already given. Stored keys
139
+ that the pack no longer declares are never rendered. The Studio session screen
140
+ shows the persisted state (known fields + next step), read-only.
141
+
142
+ The agent writes the briefing through two built-in tools, wired only when the
143
+ pack declared fields:
144
+
145
+ - `update_briefing(field:, value:)` — records a field. An undeclared `field`
146
+ returns an envelope error (`unknown field '…'; declared: …`) and nothing is
147
+ persisted; a blank `value` clears the field.
148
+ - `set_next_step(text:)` — records the agreed next step; a blank `text` clears it.
149
+
150
+ Both are deterministic in-process writes (never enveloped) and survive across
151
+ turns and resumes — a resumed conversation re-opens with the briefing intact.
152
+
86
153
  ## The provider prefix cache
87
154
 
88
155
  Two distinct caching mechanisms — don't conflate them:
89
156
 
90
157
  - **Automatic server-side prefix cache.** Some providers prefix-cache a stable
91
158
  system prefix automatically, at no cost to configure. This works **only because**
92
- the identity is at the top of the system block and the volatile
93
- `<request_context>` is at the bottom, keeping the cacheable prefix byte-stable.
159
+ the engine renders the system in two layers (below) and the volatile half sits
160
+ **under** the identity boundary, keeping the cacheable prefix byte-stable.
94
161
  Anything that injects volatile content high in the system block breaks the cache.
95
162
  - **Manual cache breakpoints (opt-in).** With `prompt_caching` on **and** a
96
163
  provider that supports explicit cache control, the builder sets one cache
@@ -100,6 +167,52 @@ Two distinct caching mechanisms — don't conflate them:
100
167
  Cache accounting surfaces as `cached_tokens` (reads) and `cache_creation_tokens`
101
168
  (writes), visible in telemetry and the Studio tokens chip.
102
169
 
170
+ ### The two layers
171
+
172
+ The system block is partitioned into two cache layers:
173
+
174
+ - **Identity** — bytes that change only on deploy/config edit: the persona
175
+ prompt (`Prompt`), the level-1 skill list (`Skill`) and the deferred-tool
176
+ catalog (`ToolSearch`). This is the cacheable prefix.
177
+ - **Volatile** — bytes that may change per turn: memory, session history,
178
+ triggered skill bodies, the `<request_context>`. Everything else.
179
+
180
+ The layer is a **provider-class contract**, not profile data: `ContextProvider`
181
+ declares `def layer = :volatile` (conservative — nothing gets pinned by
182
+ accident) and the three identity builtins override to `:identity`. A pack does
183
+ not set it — a pack reorganizes *which content goes into the Prompt provider vs
184
+ the volatile providers*. The Builder stamps the layer on every fragment at
185
+ production, and the render order is **identity first, volatile after** — a
186
+ volatile block can never land above the cache boundary, whatever its priority.
187
+ Within each partition the existing priority sort is untouched.
188
+
189
+ The engine's own `doctor` check verifies the declaration: an engine-known
190
+ volatile provider (Memory, Session, Request, SkillTrigger) that overrides to
191
+ `:identity` is an **error** (guaranteed cache kill); any other custom
192
+ `:identity` provider is a **warning** (purity unverifiable from outside — the
193
+ output must be byte-stable across turns).
194
+
195
+ ### The observable cache: fingerprints and the invalidation reason
196
+
197
+ Each turn, the Executor hashes the rendered prefix into a PII-free fingerprint
198
+ chain — one SHA-256 per system category in render order, one for the tool
199
+ schemas, one cumulative `prefix` — and compares it against the previous turn's
200
+ entry. The **invalidation reason** is the first category whose bytes changed (or
201
+ vanished); a turn whose prefix held reports nothing. History is deliberately
202
+ excluded: a new user message is a divergence every turn, which would be noise,
203
+ not a reason.
204
+
205
+ The Studio surfaces it in two places: the **session Context card** shows the
206
+ turn's cache-hit percentage and the `broke: <category>` line (plus the
207
+ `identity` marker on the category rows), and the **agent detail** carries a
208
+ cache tab with the per-agent hit series over time. The per-agent series lives
209
+ in its own capped store, because a session does not stamp its author — the
210
+ per-session trace cannot answer "cache-hit over time for *this* agent".
211
+
212
+ With the prefix stable by construction, the existing `prompt_caching` breakpoint
213
+ sits on bytes that stay put — the first (write) turn of a deployment pays the
214
+ cache write once, every subsequent turn reads.
215
+
103
216
  ## The volume
104
217
 
105
218
  Agents, prompts, skills, and tools are **data in SQLite**, not files on a volume.
data/docs/DEMO.md ADDED
@@ -0,0 +1,80 @@
1
+ ---
2
+ title: Demo data
3
+ parent: Start here
4
+ nav_order: 4
5
+ permalink: /demo/
6
+ ---
7
+
8
+ # Demo data — see every loop working at once
9
+
10
+ Most of what makes Insika worth looking at only shows up after data has
11
+ accumulated: a [funnel](OUTCOMES.md#the-outcome-funnel) with a frozen
12
+ baseline needs weeks of folded outcomes, a [refinement](REFINEMENT.md)
13
+ proposal needs a run that actually found something, an
14
+ [approval](POLICY.md#layer-2-policies-and-approvals) needs a tool call
15
+ someone is waiting on. A fresh instance shows none of that — every one of
16
+ those pages renders its empty state, which makes it hard to tell "nothing
17
+ happened yet" from "this doesn't work."
18
+
19
+ `insika demo:seed` closes that gap in one shot: it provisions a single
20
+ fictional agent (`demo-store`, an e-commerce support bot) and writes enough
21
+ realistic-looking data to see every loop at once.
22
+
23
+ ## What it creates
24
+
25
+ | Page | What you'll see |
26
+ |------|------------------|
27
+ | [Funnel](OUTCOMES.md#the-outcome-funnel) — `/studio/funnel?agent=demo-store` | 40 days of folded outcomes (`greeted → browsing → cart_started → checkout_started → purchased`) and a **frozen baseline** |
28
+ | [Follow-ups](OUTCOMES.md#follow-ups--the-seller-who-comes-back) — `/studio/followups?agent=demo-store` | one record in each state: `pending`, `fired` (one per arm, so the A/B card has something to compare), `cancelled`, `blocked` |
29
+ | [Refinement](REFINEMENT.md) — `/studio/refinement?agent=demo-store` | four runs across the lifecycle: `awaiting_approval`, `applied`, `rejected` (gate failed), `no_findings` |
30
+ | [Approvals](POLICY.md#layer-2-policies-and-approvals) — `/studio/approvals` | two pending tool calls waiting on a human, one already resolved |
31
+ | [Facts](FACTS.md) — `/studio/facts` | three distillation proposals (`pending`, `approved` — with the resulting memory fact, `rejected`) |
32
+ | [Evals](EVALS.md) — `/studio/evals?agent=demo-store` | six golden cases and a baseline run with a mix of passes and one failure |
33
+
34
+ Every record is written through the same store APIs a real turn would use
35
+ (`OutcomeStore#create` + the funnel fold, `FollowupStore#create` + its
36
+ transitions, and so on) — there is no bulk-insert shortcut, and no bundled
37
+ `.rb` script outside `lib/` (nothing here needs a checkout; it ships in the
38
+ gem).
39
+
40
+ ## Running it
41
+
42
+ From the CLI, against whichever store the rest of your commands already use
43
+ ([Running locally](RUNNING-LOCAL.md#variables-all-optional) — `INSIKA_DB`
44
+ unset means an ephemeral, in-memory store, which is a fine place to try this):
45
+
46
+ ```bash
47
+ insika demo:seed
48
+ ```
49
+
50
+ A second run is a safe no-op once `demo-store` exists; pass `--force` to seed
51
+ another batch on top (the funnel baseline recomputes cleanly, but follow-ups,
52
+ refinement runs, approvals, proposals and goldens accumulate rather than
53
+ reset — none of those stores expose a per-agent bulk-delete that a shared
54
+ "platform" tenant could call without risking another agent's data).
55
+
56
+ From the Studio, open **Settings → Demo data** and click **Seed demo data**.
57
+ It dispatches the exact same command the CLI runs — the Studio never writes a
58
+ store directly, here or anywhere else.
59
+
60
+ **This writes into whatever store the running instance already has open.**
61
+ There's no separate demo database and no isolation: point `INSIKA_DB` at a
62
+ scratch file (or leave it unset, for an ephemeral store) before seeding —
63
+ never at a deployment holding real tenant data.
64
+
65
+ ## Then look around
66
+
67
+ Once seeded, the CLI prints the same six paths listed above. If Studio isn't
68
+ running yet, boot it the way you already do — see [Running
69
+ locally](RUNNING-LOCAL.md#boot) for a checkout, or [Embedding](EMBEDDING.md)
70
+ for `Insika.agent { … }.serve` — pointed at the same `INSIKA_DB`. Studio reads
71
+ every agent's profile from the same config store, so it will show
72
+ `demo-store` next to whatever agent you're actually building, no matter which
73
+ one the running process itself defines.
74
+
75
+ ## See also
76
+
77
+ - [Refinement](REFINEMENT.md), [Facts](FACTS.md), [Evals](EVALS.md) — what
78
+ each seeded page actually means.
79
+ - [Running locally](RUNNING-LOCAL.md) — booting Studio against a durable
80
+ `INSIKA_DB`.