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/bin/insika-router ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # `insika-router` — session-sticky router. A standalone Async/
5
+ # Falcon process that sits in front of N engine backends and routes each
6
+ # request to the SAME backend every time its session id resolves to (a
7
+ # ketama-style consistent hash ring), so `WEB_CONCURRENCY=1`-per-worker's
8
+ # session guarantees (FIFO/collect/steer, docs/DEPLOY.md "The process model")
9
+ # hold at N>1 capacity without touching the engine itself.
10
+ #
11
+ # Entirely OPT-IN: an operator happy with one worker never runs this. Two
12
+ # backend-discovery modes, picked by which env var is set (never both):
13
+ #
14
+ # INSIKA_ROUTER_BACKENDS=http://host-a:9292,http://host-b:9292
15
+ # static list — the Railway shape (N local Falcon workers on known ports).
16
+ # INSIKA_ROUTER_BACKENDS_DNS=insika-headless.default.svc.cluster.local
17
+ # one hostname, re-resolved on INSIKA_ROUTER_DNS_INTERVAL (default 15s) —
18
+ # the Kubernetes shape (a headless Service, one record per ready pod).
19
+ # Requires INSIKA_ROUTER_BACKEND_PORT (the engine port on every pod).
20
+ #
21
+ # See docs/ROUTER.md for both deploy shapes end to end.
22
+
23
+ require "async"
24
+ require "async/http/server"
25
+ require "async/http/endpoint"
26
+ require "protocol/rack"
27
+
28
+ require_relative "../lib/insika/env_schema"
29
+ require_relative "../lib/insika/router"
30
+
31
+ module Insika
32
+ module CLI
33
+ module Router
34
+ module_function
35
+
36
+ def main
37
+ Insika::EnvSchema.reconcile_legacy!
38
+ pool = build_pool
39
+ app = Insika::Router::App.new(
40
+ pool: pool,
41
+ body_max_bytes: (Insika::EnvSchema.read("INSIKA_ROUTER_BODY_MAX_BYTES") || Insika::Router::App::DEFAULT_BODY_MAX_BYTES).to_i,
42
+ backend_timeout: (Insika::EnvSchema.read("INSIKA_ROUTER_BACKEND_TIMEOUT") || 10).to_i
43
+ )
44
+
45
+ host = Insika::EnvSchema.read("INSIKA_ROUTER_HOST") || "0.0.0.0"
46
+ port = (Insika::EnvSchema.read("INSIKA_ROUTER_PORT") || 9090).to_i
47
+ endpoint = Async::HTTP::Endpoint.parse("http://#{host}:#{port}")
48
+
49
+ banner(pool, host, port)
50
+ Async do |task|
51
+ pool.start_polling(task)
52
+ Async::HTTP::Server.new(Protocol::Rack::Adapter.new(app), endpoint).run
53
+ end
54
+ end
55
+
56
+ def build_pool
57
+ static = Insika::EnvSchema.read("INSIKA_ROUTER_BACKENDS")
58
+ dns = Insika::EnvSchema.read("INSIKA_ROUTER_BACKENDS_DNS")
59
+ if static.nil? == dns.nil?
60
+ abort "insika-router: set exactly one of INSIKA_ROUTER_BACKENDS or INSIKA_ROUTER_BACKENDS_DNS"
61
+ end
62
+
63
+ if static
64
+ Insika::Router::BackendPool.new(static: static.split(",").map(&:strip))
65
+ else
66
+ port = Insika::EnvSchema.read("INSIKA_ROUTER_BACKEND_PORT")
67
+ abort "insika-router: INSIKA_ROUTER_BACKEND_PORT is required with INSIKA_ROUTER_BACKENDS_DNS" if port.nil?
68
+
69
+ Insika::Router::BackendPool.new(
70
+ dns: dns, dns_port: port.to_i,
71
+ dns_interval: (Insika::EnvSchema.read("INSIKA_ROUTER_DNS_INTERVAL") || 15).to_i
72
+ )
73
+ end
74
+ end
75
+
76
+ def banner(pool, host, port)
77
+ puts "\e[1mInsika Router\e[0m — session-sticky proxy"
78
+ puts " http://#{host}:#{port}/up → health (also this process's own liveness)"
79
+ mode = pool.dns? ? "dns" : "static"
80
+ puts " backends (#{mode}): #{pool.backends.join(', ')}"
81
+ puts " Ctrl-C to stop."
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ Insika::CLI::Router.main
data/docs/AGENTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Agents
3
- parent: Build an agent
3
+ parent: Core concepts
4
4
  nav_order: 1
5
5
  permalink: /agents/
6
6
  ---
@@ -41,7 +41,7 @@ ergonomics, not in what they produce.
41
41
  re-import that drops a tool or skill also removes it. `DELETE /v1/agents/:id`
42
42
  removes an agent.
43
43
  - **Studio** — create and edit an agent by hand in the control UI (Config /
44
- Prompts / Skills / Memory / History tabs), backed by the same commands.
44
+ Prompts / Skills / Memory / Outcomes / Cache / History / **loops** tabs), backed by the same commands.
45
45
 
46
46
  Creating an agent validates its id (required, must be unique) and its subagent
47
47
  graph (cycle/depth — see [subagents](#delegation-subagents)) **before**
@@ -70,6 +70,23 @@ injected text as the customer repeating themselves. See
70
70
  ## The AgentProfile
71
71
 
72
72
  A profile is built through one front door — `AgentProfile.build(id:, model: nil, …)`.
73
+ `model`/`provider` are a straight pass-through to [RubyLLM](https://rubyllm.com) — Insika
74
+ keeps no allowlist of "supported" models. Whatever RubyLLM's installed version
75
+ reaches, an agent can name: today that's 13 provider adapters (Anthropic, OpenAI,
76
+ Gemini, DeepSeek, Bedrock, Vertex AI, Azure, Mistral, xAI, OpenRouter,
77
+ Perplexity, Ollama, GPUStack) and every model each one exposes — see
78
+ [rubyllm.com/available-models](https://rubyllm.com/available-models/) for the
79
+ current, live catalog. Upgrading the `ruby_llm` gem is the only thing that ever
80
+ widens this list; no Insika code changes with it.
81
+
82
+ Wire a provider's credentials once in the Studio's **Settings → LLM providers**
83
+ (or `POST /v1/settings`) — `api` is any slug RubyLLM recognizes, and
84
+ `LLMConfigurator` applies it by reflection (`<api>_api_key=`, `<api>_api_base=`),
85
+ so a provider RubyLLM doesn't expose an accessor for is skipped, never a hard
86
+ error. To *restrict* which models an agent may use — the opposite direction —
87
+ declare `model_policy: { allow: [refs] }` on its profile (exact `"provider/model"`
88
+ refs or a `"provider/*"` wildcard); absent means no fence, every configured
89
+ model is fair game.
73
90
  A prompt file is **text**. Passing a structured value where the markdown belongs —
74
91
  a `{"content": …}` wrapper in a pack, or a store entry read and written back — is
75
92
  rejected, not coerced: `to_s` on a Hash produces Ruby's `#inspect`, and a prompt made
@@ -97,6 +114,31 @@ the next tool-batch boundary: "you already ran this, answer with what you
97
114
  have"). A repeat after the warning aborts the turn like `max_tool_calls` does.
98
115
  Set it below 2 to switch it off.
99
116
 
117
+ ### `tool_persistence` — don't give up on the first empty result
118
+
119
+ The loop guard's mirror image. `max_tool_repeat` stops the model from repeating
120
+ the *same* call; `tool_persistence` stops it from giving up after *one* weak
121
+ call. When ON (the default), the engine appends a short **"Tool discipline"**
122
+ block after the agent's identity in the system prompt: a weak or empty tool
123
+ result means *try again with a different approach — a rephrased query, a
124
+ synonym, a broader term — before telling the user you found nothing* (and don't
125
+ narrate the retries); a tool error means *read it and fix the arguments*, never
126
+ repeat the exact same call. Without it, a search that returns 0 results reads as
127
+ final and the model answers "I couldn't find it" when a synonym one call away
128
+ would have.
129
+
130
+ This is the **one default-ON profile flag** — every field above is opt-in, this
131
+ one is opt-out, because the behavior is the proven default and the exception is
132
+ the thing worth declaring:
133
+
134
+ ```ruby
135
+ tool_persistence false # remove the block for this agent
136
+ ```
137
+
138
+ The block is a byte-stable constant, so `prompt_caching` stays effective: the
139
+ deploy that introduces it costs one cache write per agent, and every turn after
140
+ that hits as before.
141
+
100
142
  ### Why some limits are missing from that list
101
143
 
102
144
  `chat_rate_limit`, `agent_token_ceiling`, `queue_mode`, `debounce_ms`,
@@ -219,6 +261,12 @@ separately lives in one event, emitted when the window closes:
219
261
  Times and counts, never content. That is what answers "the customer says they
220
262
  sent the order number" without keeping a throwaway task per fragment.
221
263
 
264
+ > **`steer` also collects at the door.** The two windows are the same policy's
265
+ > halves, not two modes: a `steer` agent that set a `debounce_ms` also merges the
266
+ > fragments that land before the turn starts. The window value is what
267
+ > an operator replaces the legacy pre-batch buffer with — `steer` catches anything
268
+ > that arrives after the turn started, the door window the fragments before it.
269
+
222
270
  #### `steer` — the message arrives while the turn is already running
223
271
 
224
272
  `collect` only ever touches a turn that has **not started**. Once the agent is
@@ -339,7 +387,9 @@ Three capabilities invert the default — `nil`/absent means **OFF**, not "all":
339
387
  setting, never "everything on"). `tool_output_compression` is a fourth: opt-in
340
388
  mechanical dedupe of repeated tool results in the history (see
341
389
  [Context](CONTEXT.md#compaction-is-not-wired--except-the-mechanical-dedupe)),
342
- off by default because it changes what the model sees.
390
+ off by default because it changes what the model sees. And one flag inverts the
391
+ other way: `tool_persistence` is **ON unless you set it to `false`** (see
392
+ [`tool_persistence`](#tool_persistence--dont-give-up-on-the-first-empty-result)).
343
393
 
344
394
  ### Declaring what this deployment has
345
395
 
@@ -351,167 +401,9 @@ has promotions" from data is how a test suite starts lying.
351
401
 
352
402
  ## The five access layers
353
403
 
354
- What an agent may do is layered. Each layer is independent, opt-in where it
355
- matters, and editable hot.
356
-
357
- ### Layer 1: Tools (what it can call)
358
-
359
- `tools_allow` / `tools_deny` / `tools_allow_groups` decide which tools enter the
360
- turn's tool-loop, enforced by the tool-allowlist policy. See [Tools](TOOLS.md)
361
- for how tools are defined and registered, and [`examples/data-tool/`](https://github.com/guizaols/insika/tree/main/examples/data-tool/).
362
-
363
- ### Layer 2: Policies and approvals
364
-
365
- Policies are named entries evaluated before the turn runs. Builtins cover
366
- tool-, skill-, and workflow-allowlisting, plus **`ApprovalRequired`** — which
367
- does not allow or deny but *tags* a tool as needing human approval. Set
368
- `approvals_required: [tool names]`; the gate then fires when the model tries to
369
- call that tool, suspending the turn until an operator approves it in the Studio.
370
- See [Security](SECURITY.md#human-approval).
371
-
372
- ### Layer 3: Guardrails (content safety)
373
-
374
- `guardrails` configures input/output content safety per agent — **opt-in**, so an
375
- agent that says nothing gets a conservative default (deterministic detectors on,
376
- LLM moderator off). See [Security](SECURITY.md#guardrails) and
377
- [`examples/guardrails/`](https://github.com/guizaols/insika/tree/main/examples/guardrails/).
378
-
379
- ### Layer 4: Edge limits (flood and spend control)
380
-
381
- Two independent, opt-in ceilings, enforced *before* the model is ever called —
382
- opt-in everywhere except on a public channel, where `chat_rate_limit` is
383
- [required](CHANNELS.md#a-rate-limit-is-required-not-suggested) and the
384
- [web widget](CHANNELS.md#the-web-widget) refuses to serve without one:
385
-
386
- - **`chat_rate_limit`** — turn attempts per session per `chat_rate_window`.
387
- - **`agent_token_ceiling`** — total tokens per agent per `agent_token_window`.
388
-
389
- On breach the turn halts gracefully with a configurable `limit_response` and
390
- **zero LLM calls**. Windows are set at the platform level; the ceilings can be set
391
- per agent (blank inherits the platform value, `0` explicitly disables it).
392
-
393
- > ⚠️ The token window default is **86400 (daily)**. To express "500k tokens per
394
- > **hour**", set `agent_token_window = 3600` explicitly. A per-agent key that is
395
- > *present but nil* reads as OFF for that agent — leave the key **absent** to
396
- > inherit. See [Security](SECURITY.md#edge-limits).
397
-
398
- #### Calendar budgets — the daily/monthly cost wall
399
-
400
- A third, opt-in ceiling for the *billing* shape the windows above cannot express:
401
- a spend cap over a CALENDAR day or month, per `(tenant, agent)` when
402
- multi-tenant. Data on the profile (DSL `budget` or the pack's `budget` key):
403
-
404
- ```ruby
405
- budget daily: 100_000, monthly: 2_000_000, soft: false # or soft: true
406
- ```
407
-
408
- - Tokens count the **billed** spend — `input + output + cached + cache_creation`
409
- (the cached prefix is the bulk of the bill, not an afterthought).
410
- - **Hard** (the default, `soft` absent/false): a turn that arrives with the
411
- window's spend already at/over the cap **fails** with the typed
412
- `Insika::BudgetExceeded` — the envelope reads `budget_exceeded` +
413
- `retry_after` (seconds until the window rolls). It is NOT a customer reply; it
414
- is an operator signal.
415
- - **Soft** (`soft: true`): the same turn RUNS — crossing the cap emits one
416
- `budget_warning` event per window and injects a note into the context (the
417
- model sees it, the transcript does not).
418
- - Either way, crossing `alert_at` (default `0.8` of the cap) fires the same
419
- warning **before** the wall, once per window.
420
-
421
- > ⚠️ Unlike the ceilings above, the cap that counts is per **calendar** window —
422
- > a `daily` budget rolls at UTC midnight, a `monthly` one on the 1st, whatever
423
- > the sun. `agent_token_window` is a fixed seconds window and cannot express
424
- > "the day resets at midnight".
425
-
426
- #### Reliability — retries, fallback, circuit breaker (WS3)
427
-
428
- The provider interaction is a single attempt by default (RubyLLM's own 2
429
- transport retries aside). For a store that cannot have a dead model take the
430
- chat down, the reliability policy is DATA on the profile:
431
-
432
- ```ruby
433
- reliability retries: 2, backoff: "exponential",
434
- fallback: ["openai/gpt-4o-mini"],
435
- circuit_breaker: { after: 10, within: 60, cooldown: 300 }
436
- ```
437
-
438
- - **Retries** — transient failures (`:retryable` / `:rate_limited_*` per the
439
- error classification) retry with exponential backoff, up to `retries`.
440
- A `:fatal` (auth, billing, bad request) is NEVER retried or rotated. Each
441
- attempt runs on a fresh chat — the customer-visible answer comes only from
442
- the attempt that returns.
443
- - **Fallback** — after a node's retries, the turn ROTATES to the next model in
444
- the chain: the profile's `fallback` refs first, then the platform
445
- `fallback_models`. The turn's usage is attributed to the model that actually
446
- spoke (`model_source: "fallback"`).
447
- - **Circuit breaker** — per `(tenant, provider/model)`: `after` failures within
448
- `within` seconds open the circuit; while open, the turn fail-fasts with the
449
- typed `circuit_open` + `retry_after` (remaining cooldown) and the provider is
450
- never touched. After `cooldown` a half-open trial closes the circuit on
451
- success or reopens it on failure.
452
- - **`timeout`** — per-attempt ceiling (default 30s), counted as a retryable
453
- failure.
454
-
455
- Absent `reliability` = the plain single attempt, byte-for-byte today's
456
- behavior.
457
-
458
- #### Operator alerts — the webhook (WS6)
459
-
460
- Three operational events — `budget_warning`, `breaker_open`, `delivery_failed` —
461
- can be answered per agent with a webhook:
462
-
463
- ```ruby
464
- alerts webhook: "https://ops.example.com/insika-alerts"
465
- ```
466
-
467
- When present, each such event is POSTed to the URL as JSON (the event's
468
- type/data/meta, plus the agent). Delivery rides the same outbox + claim +
469
- bounded-retry pipeline as channel answers — at-most-once, crashed deliveries
470
- recovered at boot. The engine transports the event and does not interpret it: a
471
- Slack/CRM adapter is the consumer's. Absent `alerts` = nothing is sent.
472
-
473
- Separately, with `INSIKA_TURN_TIMING`, the provider's **live TTFB** is carried in
474
- the streaming envelope: the first content chunk emits an `insika.ttft` frame
475
- (`ttft_ms`) on `/v1/responses`, alongside the per-turn `timing` breakdown on the
476
- final `response.completed`. Additive and opt-in — absent by default.
477
-
478
- ### Layer 5: Reasoning (thinking)
479
-
480
- Controls the model's thinking budget, resolved by precedence
481
- **Chat > Agent > Model > Global** (first non-blank wins):
482
-
483
- | Scope | Where |
484
- |-------|-------|
485
- | Chat | session var `__llm__.thinking` |
486
- | Agent | `profile.params["thinking"]` |
487
- | Model | platform `model_params[<ref>].thinking` |
488
- | Global | platform `thinking` |
404
+ What an agent may do is layered: which tools it may call, which calls need a human, content safety, edge limits on flood and spend, and how much it may think. See [Limits and policy](POLICY.md).
489
405
 
490
- Values: `off | on | low | medium | high`. `off`/`on` toggle thinking; the effort
491
- levels map to the provider's thinking-effort parameter. This is a control
492
- primitive, not a latency lever — turning reasoning off does not necessarily speed
493
- up a turn, because most of a turn's latency is the provider itself, not thinking.
494
-
495
- Whether the reasoning ever reaches the **customer** is a separate switch, off by
496
- default:
497
-
498
- ```ruby
499
- edge_stream thinking: true, intermediate: false
500
- ```
501
-
502
- `thinking` is the provider's reasoning; `intermediate` is the model narrating its
503
- own tool loop ("let me look that up"). Both are always on the event stream for the
504
- Studio and the trace — this decides only whether `/v1/responses` translates them,
505
- and each opted-in channel gets its own frame type, never the answer's. See
506
- [Architecture](ARCHITECTURE.md#what-crosses-the-edge).
507
-
508
- > ⚠️ Turn it on knowing your consumer. One that concatenates every text delta into
509
- > a single message — a WhatsApp adapter — will only be affected once it learns to
510
- > read the new frames, and when it does, the deliberation is what the customer
511
- > reads. That is the operator's call, which is why it is neither a default nor a
512
- > global.
513
-
514
- ## The stuck signal — "I cannot proceed" (WS5)
406
+ ## The stuck signal "I cannot proceed"
515
407
 
516
408
  The engine doesn't decide what "I can't help you" means — the consumer does. What
517
409
  the engine provides is the deterministic signal, so that a product wanting
@@ -562,6 +454,81 @@ allowlist, one or more `proposers`, a token `budget`, and a gate that replays th
562
454
  golden set before anything reaches a human. All of it is in
563
455
  [Refinement](REFINEMENT.md); none of it is on until you name it.
564
456
 
457
+ ## Distillation of customer facts
458
+
459
+ `distill` configures how finished, idle customer conversations are read back as
460
+ proposed facts — the human-gated loop documented in [Facts](FACTS.md). Pack
461
+ data, `refinement:`'s shape, and absent = off for that agent:
462
+
463
+ ```ruby
464
+ distill enabled: true,
465
+ idle_hours: 6, # how idle a session must be before it distills
466
+ min_messages: 3, # a shorter session distills noise
467
+ max_proposals: 10 # cap per session pass
468
+ # prompt: "<what counts as a fact for THIS store>" (the forge's half)
469
+ # model: "<ref — absent = the platform utility_model>"
470
+ ```
471
+
472
+ Nothing is ever applied automatically: the engine writes **proposals**, the
473
+ operator approves/rejects/dismisses them on the Studio **Facts** page (the
474
+ latch: a dismissed or rejected tuple is never proposed again), and an approval
475
+ writes the fact to the customer's memory cell stamped
476
+ `distilled:<session_ref>` through an optimistic CAS — an approval never
477
+ silently overwrites an operator edit. Sessions are the only candidates, and
478
+ the distiller rides the platform `utility_model`, never a new model slot.
479
+
480
+ ## Harvest of skills from real traffic
481
+
482
+ `harvest` configures how finished, idle conversations are read back as
483
+ proposed **SKILLS** for the agent's playbook — the human-gated loop documented
484
+ in [Harvest](HARVEST.md). Pack data, `distill:`'s shape, and absent = off for
485
+ that agent:
486
+
487
+ ```ruby
488
+ harvest enabled: true,
489
+ negative_list: [ { rule: "no-competitor-prices", pattern: "concorrente" } ],
490
+ miner: { model: "deepseek-v4-flash", # absent = the platform utility_model
491
+ window: { last_sessions: 200 } },
492
+ idle_hours: 24,
493
+ min_messages: 3
494
+ # prompt: "<what a harvestable skill is for THIS store>" (the forge's half)
495
+ ```
496
+
497
+ The loop reads only finished traffic (the fork is structural — the mining
498
+ writes nothing to the sessions it read), filters every proposal through the
499
+ negative list and the evidence ledger (product claims must reference IDs the
500
+ origin sessions actually saw — an agent without `grounding.matcher.sku` does
501
+ not mine at all), scores survivors with a double gate (the eval replay against
502
+ the clone's golden set, judges mandatory in three shapes; the
503
+ conversion "not worse" check against the frozen funnel baseline), and lands
504
+ a skill **only after a human approves** — snapshot-first, append-only
505
+ promotion log, deterministic rollback. Nothing is ever applied automatically.
506
+
507
+ ## Knowledge from finished conversations
508
+
509
+ `knowledge` configures the post-turn learning loop documented in
510
+ [Knowledge](KNOWLEDGE.md): after a turn completes, the engine extracts durable
511
+ **concepts** — facts, procedures, policies, objections — and persists them for
512
+ the agent, separate from any one customer's memory. Pack data, `distill:`'s
513
+ shape, and absent = off for that agent:
514
+
515
+ ```ruby
516
+ knowledge extract: true,
517
+ types: %w[fact policy objection] # what the extractor may emit
518
+ # prompt: "<what counts as a concept for THIS store>" (the forge's half)
519
+ # model: "<ref — absent = the platform utility_model>"
520
+ ```
521
+
522
+ The model proposes `name`/`description`/`type`/`body`; the engine stamps
523
+ `provenance`/`confidence`/`sources`/timestamps itself (a model-authored one
524
+ of those is dropped, never trusted) and redacts the body for PII. Writing a
525
+ concept name that already exists never blindly overwrites: the engine
526
+ decides same claim (bump the evidence), related claim (merge, one extra
527
+ model call), or contradicting claim (never merged — appended under a
528
+ heading, confidence drops, a human resolves it in the Studio's Knowledge
529
+ page). Nothing is injected into a turn's prompt yet — see
530
+ [Knowledge](KNOWLEDGE.md#whats-not-here-yet) for what's not here.
531
+
565
532
  ## Delegation (subagents)
566
533
 
567
534
  An agent can delegate to **subagents**: named child agents it may invoke as a
@@ -608,9 +575,108 @@ next turn with no restart. See [Deploy](DEPLOY.md) for the durable-volume setup
608
575
  [Context](CONTEXT.md#the-volume) for why editing a committed file does *not* change
609
576
  a running agent.
610
577
 
578
+ ## Media
579
+
580
+ Photos, voice notes and documents travel through the message contract as additive content parts, and the engine can hand generated images back the same way. See [Media](MEDIA.md).
581
+
582
+ ## Customer-scoped memory and the right to be forgotten
583
+
584
+ Memory is naturally **per customer, not per tenant**. A message that carries a
585
+ `customer` key moves the engine's memory scope to that person:
586
+
587
+ ```bash
588
+ curl -X POST /v1/messages?stream=false -H "Authorization: Bearer $TOKEN" \
589
+ -d '{ "agent": "store-support", "session_id": "chat-7",
590
+ "customer": "c-123", "message": "cadê meu pedido" }'
591
+ ```
592
+
593
+ - **Scope** — with `customer` present, the `remember` tool and the `<memory>`
594
+ block read/write the `[tenant:]customer` cell: two customers under the same
595
+ tenant never see each other, and the `<request_context>` tenant label (the
596
+ merchant) is untouched. Absent `customer` = today's per-tenant/per-chat
597
+ behavior.
598
+ - **Right to be forgotten** — `POST /v1/commands/forget_customer` (operator)
599
+ purges the customer's memory cell, their sessions and everything those
600
+ sessions left behind — per-session traces, the tasks (the message text lives
601
+ in the persisted command), their checkpoints (the transcript) and the outbox
602
+ deliveries (the answer as it was handed to the channel) — and nothing else's:
603
+ `{ "customer": "c-123", "tenant": "acme" }`. **Name the tenant**: the
604
+ operator credential carries none, and without one the purge means the whole
605
+ deployment (the untagged memory cell, plus that customer's sessions in every
606
+ tenant) — right for a single-tenant deployment, never what a multi-tenant
607
+ operator means. Facts also support an optimistic CAS write
608
+ (`replace_if_revision`) for an integration that must not clobber a concurrent
609
+ edit.
610
+ - **Tenant deletion** — `POST /v1/commands/delete_tenant_data` (operator)
611
+ purges EVERYTHING the engine holds about one tenant: its sessions and their
612
+ whole footprint (traces, tasks, checkpoints, outbox deliveries), every memory
613
+ cell under the tenant (its own + the customer cells — enumerated from the
614
+ store, so even a cell whose session was already deleted goes), its outcome
615
+ records and its artifacts (a report is content, never kept behind an
616
+ offboarded tenant): `{ "tenant": "acme" }`. Its API tokens are **revoked
617
+ first** (before the sweep): an offboarded tenant whose credentials still
618
+ resolved kept authenticating and could open a new session over the erasure.
619
+ The tenant string is the isolation boundary; a neighbour is untouched.
620
+ - **Retention** — the age-based counterpart, as data: the settings key
621
+ `retention_days` (Integer days; absent/0 = OFF, the engine never sweeps by
622
+ default). The tick's daily sweep (at most once per 24 h, behind the same
623
+ single-key claim the stale-task sweep uses) purges sessions (+traces),
624
+ terminal tasks (+checkpoints), delivered/failed outbox records, memory
625
+ facts/notes and outcomes older than the window. A non-terminal task is never
626
+ touched — the Recovery sweep owns those lives — and neither is a delivery
627
+ still owed to somebody. One thing the same daily pass sweeps **regardless of
628
+ `retention_days`**: the budget counter cells whose window already rolled over
629
+ (and their once-per-window alert markers). Those are engine bookkeeping, not
630
+ customer content, and nothing else ever collected them. Artifacts (reports)
631
+ likewise expire on their **own** knob, `artifact_ttl_days` (settings; absent
632
+ = OFF) — the guarantee that PII inside a report dies even in a deployment
633
+ that keeps its conversations forever. See [Artifacts](ARTIFACTS.md).
634
+
635
+ ## Outcomes and follow-ups
636
+
637
+ What the agent's traffic was worth in business terms, and the tool that lets it come back later on a promise it made. See [Outcomes and follow-ups](OUTCOMES.md).
638
+
639
+ ## Schedules — recurring turns the engine fires
640
+
641
+ The operator's counterpart to follow-ups: a turn **nobody sends** — the daily
642
+ report at 22:00, the eval sweep every night. Declared per agent as pack data
643
+ (`schedule "daily_report", cron: …, tz: …, message: …` or `every: N`), edited
644
+ hot in the Studio's Schedules config group, fired by the engine's own tick
645
+ one turn per claim window, never queued, no catch-up after a downtime (each
646
+ missed window is a recorded skip, visible in the Studio):
647
+
648
+ ```ruby
649
+ agent = Insika.agent("reporter") do
650
+ schedule "daily_report", cron: "0 22 * * *", tz: "America/Sao_Paulo",
651
+ message: "Run the daily report now.",
652
+ overrides: { turn_timeout: 900, max_tool_calls: 200 }
653
+ end
654
+ ```
655
+
656
+ The run is a first-class turn stamped `origin: "scheduled"`; `session_mode:
657
+ "new"` gives it a fresh session per run (the report shape), `"fixed"` a
658
+ standing one; per-run `overrides` raise the chat-time ceilings a report needs.
659
+ A hard calendar budget at its cap skips instead of burning the store's tokens.
660
+ Distinct by shape and by law from the follow-up tool. See
661
+ [Schedules](SCHEDULING.md).
662
+
663
+ ## Artifacts — a report the agent can hand you a URL to
664
+
665
+ A scheduled report's natural output is not a message but a page — tables,
666
+ sections, inline charts. The `save_artifact` tool (a registry tool, in the
667
+ agent's `tools_allow`) gives the agent a durable destination: it hands in
668
+ title + content and gets a URL back, which it can include in a channel message
669
+ ("today's report: <url>"). The report is stored (one per run, the listing is
670
+ the history), served under `/studio/artifacts/…` inside a sandboxed iframe,
671
+ optionally shared outside the Studio via an expiring signed link
672
+ (`INSIKA_ARTIFACT_SIGNING_KEY`). The tenant binding is inherited from the
673
+ saving agent — never a parameter the model types. Artifact content is LLM
674
+ output and is served as untrusted. See [Artifacts](ARTIFACTS.md).
675
+
611
676
  ## See also
612
677
 
613
678
  - [Tools](TOOLS.md) — define, register, and troubleshoot tools.
679
+ - [Artifacts](ARTIFACTS.md) — the report destination: the tool, the routes, the signed link.
614
680
  - [Skills](SKILLS.md) — progressive playbooks an agent loads on demand.
615
681
  - [Context](CONTEXT.md) — what fills a turn's prompt, and memory.
616
682
  - [Security](SECURITY.md) — guardrails, sandbox, approvals, edge limits.
data/docs/API.md ADDED
@@ -0,0 +1,56 @@
1
+ ---
2
+ title: The /v1 API
3
+ parent: Integrate
4
+ nav_order: 1
5
+ permalink: /api/
6
+ ---
7
+
8
+ # The /v1 API — the frozen compatibility contract
9
+
10
+ The HTTP surface is the drop-in OpenAI-Responses-compatible contract: a client
11
+ that speaks `/v1` today speaks it tomorrow. This page is the WRITTEN promise
12
+ — the mechanical half is the version gate in the server, and the
13
+ two cannot drift (a spec pins the gate's version to the date below).
14
+
15
+ ## The surface
16
+
17
+ | Route | What it is |
18
+ |-------|------------|
19
+ | `POST /v1/responses` | the OpenAI-Responses-compatible turn ingress (streaming SSE) |
20
+ | `POST /v1/messages` | the channel-style message ingress (one turn per message, ack-fast) |
21
+ | `POST /v1/agents` | provisioning by definition/pack (idempotent, authoritative) |
22
+ | `GET/DELETE /v1/agents` | list / remove agents |
23
+ | `POST /v1/sessions` | create a session explicitly |
24
+ | `POST /v1/outcomes` | record a business outcome (`conversion`, `escalation`, any label) |
25
+ | `GET /v1/outcomes` | last outcome per agent + the per-day series |
26
+ | `POST /v1/commands/...` | operator commands (`forget_customer`, `delete_tenant_data`, the follow-up mutations) |
27
+ | `GET /v1/vitals` | process health/uptime vitals |
28
+ | `GET /v1/...` | the onboarding surface (`start.md`, `models.json`, `/docs/<name>.md`) |
29
+
30
+ The turn endpoints speak the OpenAI `response.completed` wire format; the
31
+ `Insika-Version` header declares the compatibility vintage of a request.
32
+
33
+ ## The compatibility rule
34
+
35
+ **Request and response shapes are stable; changes are additive-only.** A new
36
+ field on a request or response is a release. Removing, renaming or re-typing a
37
+ field is a BREAKING change and requires, together, all three:
38
+
39
+ 1. a new `Insika-Version` entry in the server's known-versions gate (an
40
+ unknown value is refused with 400 before auth — fail-closed);
41
+ 2. a route branch that serves both vintages (a compatibility branch), so
42
+ an old client keeps working while the new shape rolls;
43
+ 3. a new `**Frozen as of:**` line below.
44
+
45
+ Additive changes need none of the three.
46
+
47
+ **Frozen as of: 2026-08-08** — the date that is the 1.0 promise. The gate's
48
+ first known version, pinned by `spec/insika/server/version_gate_spec.rb`; the
49
+ release writes the 1.0 freeze date here at release time.
50
+
51
+ ## The 400-on-unknown rule
52
+
53
+ A request carrying an `Insika-Version` the gate does not know is refused with
54
+ `400 Bad Request` BEFORE authentication — a client that speaks a future or
55
+ misspelled vintage learns immediately, and an unauthenticated probe cannot
56
+ distinguish the gate from auth (fail-closed, constant-time).
data/docs/ARCHITECTURE.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Architecture
3
- parent: Understand the idea
4
- nav_order: 2
3
+ parent: Start here
4
+ nav_order: 3
5
5
  permalink: /architecture/
6
6
  ---
7
7
 
@@ -319,6 +319,7 @@ validator as the after-task hook, so both roots enforce content safety identical
319
319
  | Tools (data/manifest/MCP) | `lib/insika/tool_definition.rb`, `tool_manifest.rb`, `mcp_tool_ingestor.rb` |
320
320
  | Plugin loading (boot) | `lib/insika/plugin.rb`, `lib/insika/plugin/loader.rb` |
321
321
  | Refinement (traffic → report) | `lib/insika/refinement/*`, `lib/insika/refinement_store.rb` |
322
+ | Post-turn learning (facts, skills, knowledge — extracted from finished conversations) | `lib/insika/distill.rb`, `lib/insika/harvest.rb`, `lib/insika/knowledge.rb`, `lib/insika/knowledge_store.rb`; the per-turn hook lives in `Executor#persist_turn`, next to `finalize_delegation` |
322
323
  | Evals (cases, judges, gate) | `lib/insika/evals/*`, `lib/insika/golden_store.rb`; `evals/run.rb` is the CLI |
323
324
  | HTTP/SSE surface | `lib/insika/server/*` |
324
325