insika 0.0.1 → 0.2.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 (277) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +361 -0
  3. data/LICENSE +21 -0
  4. data/README.md +136 -2
  5. data/bin/insika +366 -0
  6. data/docs/AGENTS.md +618 -0
  7. data/docs/ARCHITECTURE.md +333 -0
  8. data/docs/BENCHMARK.md +114 -0
  9. data/docs/CHANNELS.md +453 -0
  10. data/docs/CONTEXT.md +117 -0
  11. data/docs/DEPLOY.md +354 -0
  12. data/docs/EMBEDDING.md +198 -0
  13. data/docs/EVALS.md +273 -0
  14. data/docs/LOADTEST.md +232 -0
  15. data/docs/OBSERVABILITY.md +374 -0
  16. data/docs/PLUGINS.md +211 -0
  17. data/docs/REFINEMENT.md +477 -0
  18. data/docs/RELEASING.md +70 -0
  19. data/docs/RUNNING-LOCAL.md +153 -0
  20. data/docs/SANDBOX.md +114 -0
  21. data/docs/SECURITY.md +375 -0
  22. data/docs/SKILLS.md +284 -0
  23. data/docs/TOOLS.md +302 -0
  24. data/docs/WHY.md +137 -0
  25. data/docs/WORKFLOWS.md +225 -0
  26. data/docs/build.md +14 -0
  27. data/docs/index.md +68 -0
  28. data/docs/onboarding/start.md +126 -0
  29. data/docs/operate.md +12 -0
  30. data/docs/ship.md +10 -0
  31. data/docs/understand.md +10 -0
  32. data/lib/insika/agent_file_store.rb +125 -0
  33. data/lib/insika/agent_profile.rb +255 -0
  34. data/lib/insika/alert_dispatcher.rb +139 -0
  35. data/lib/insika/allowlist.rb +28 -0
  36. data/lib/insika/baseline_store.rb +74 -0
  37. data/lib/insika/budget_ledger.rb +135 -0
  38. data/lib/insika/capability/resolved_tool.rb +34 -0
  39. data/lib/insika/capability_registry.rb +112 -0
  40. data/lib/insika/channel_delivery.rb +153 -0
  41. data/lib/insika/channel_registry.rb +30 -0
  42. data/lib/insika/channels/relay.rb +178 -0
  43. data/lib/insika/channels/web/widget.js +283 -0
  44. data/lib/insika/channels/web.rb +211 -0
  45. data/lib/insika/channels/webhook.rb +58 -0
  46. data/lib/insika/chat_builder.rb +303 -0
  47. data/lib/insika/checkpoint.rb +13 -0
  48. data/lib/insika/checkpoint_store.rb +153 -0
  49. data/lib/insika/circuit_state.rb +114 -0
  50. data/lib/insika/coercion.rb +58 -0
  51. data/lib/insika/command.rb +32 -0
  52. data/lib/insika/command_bus.rb +39 -0
  53. data/lib/insika/commands/agent_payload.rb +43 -0
  54. data/lib/insika/commands/approve_action.rb +46 -0
  55. data/lib/insika/commands/cancel_task.rb +33 -0
  56. data/lib/insika/commands/create_agent.rb +54 -0
  57. data/lib/insika/commands/create_session.rb +67 -0
  58. data/lib/insika/commands/delete_agent.rb +33 -0
  59. data/lib/insika/commands/delete_agent_file.rb +50 -0
  60. data/lib/insika/commands/delete_data_tool.rb +33 -0
  61. data/lib/insika/commands/delete_llm_provider.rb +36 -0
  62. data/lib/insika/commands/delete_mcp.rb +30 -0
  63. data/lib/insika/commands/delete_skill.rb +43 -0
  64. data/lib/insika/commands/delete_system_file.rb +29 -0
  65. data/lib/insika/commands/gate_refinement.rb +245 -0
  66. data/lib/insika/commands/import_mcp_tools.rb +48 -0
  67. data/lib/insika/commands/import_tools.rb +81 -0
  68. data/lib/insika/commands/issue_tenant_token.rb +41 -0
  69. data/lib/insika/commands/memory_add_note.rb +32 -0
  70. data/lib/insika/commands/memory_forget_fact.rb +32 -0
  71. data/lib/insika/commands/memory_put_fact.rb +35 -0
  72. data/lib/insika/commands/pause_task.rb +29 -0
  73. data/lib/insika/commands/resolve_refinement.rb +126 -0
  74. data/lib/insika/commands/restore_agent_file.rb +36 -0
  75. data/lib/insika/commands/restore_data_tool.rb +34 -0
  76. data/lib/insika/commands/restore_system_file.rb +31 -0
  77. data/lib/insika/commands/resume_task.rb +85 -0
  78. data/lib/insika/commands/revoke_token.rb +39 -0
  79. data/lib/insika/commands/rotate_tenant_token.rb +43 -0
  80. data/lib/insika/commands/run_refinement.rb +133 -0
  81. data/lib/insika/commands/send_message.rb +150 -0
  82. data/lib/insika/commands/set_agent_tools.rb +39 -0
  83. data/lib/insika/commands/set_skill_agents.rb +112 -0
  84. data/lib/insika/commands/trigger_workflow.rb +80 -0
  85. data/lib/insika/commands/update_agent.rb +49 -0
  86. data/lib/insika/commands/update_settings.rb +33 -0
  87. data/lib/insika/commands/upsert_llm_provider.rb +34 -0
  88. data/lib/insika/commands/upsert_mcp.rb +32 -0
  89. data/lib/insika/commands/write_agent_file.rb +57 -0
  90. data/lib/insika/commands/write_data_tool.rb +43 -0
  91. data/lib/insika/commands/write_golden.rb +58 -0
  92. data/lib/insika/commands/write_skill.rb +60 -0
  93. data/lib/insika/commands/write_system_file.rb +31 -0
  94. data/lib/insika/config_store.rb +89 -0
  95. data/lib/insika/context/builder.rb +166 -0
  96. data/lib/insika/context/catalog_provider.rb +23 -0
  97. data/lib/insika/context/fragment.rb +43 -0
  98. data/lib/insika/context/priority.rb +30 -0
  99. data/lib/insika/context/provider.rb +19 -0
  100. data/lib/insika/context/providers/memory.rb +60 -0
  101. data/lib/insika/context/providers/prompt.rb +105 -0
  102. data/lib/insika/context/providers/request.rb +32 -0
  103. data/lib/insika/context/providers/session.rb +123 -0
  104. data/lib/insika/context/providers/skill.rb +24 -0
  105. data/lib/insika/context/providers/skill_trigger.rb +128 -0
  106. data/lib/insika/context/providers/tool_search.rb +20 -0
  107. data/lib/insika/context_trace_store.rb +92 -0
  108. data/lib/insika/delegation_store.rb +153 -0
  109. data/lib/insika/doctor.rb +539 -0
  110. data/lib/insika/dsl/definition.rb +55 -0
  111. data/lib/insika/dsl/runtime.rb +382 -0
  112. data/lib/insika/dsl/server_boot.rb +98 -0
  113. data/lib/insika/dsl/system.rb +93 -0
  114. data/lib/insika/dsl/workflow_adapter.rb +59 -0
  115. data/lib/insika/dsl.rb +364 -0
  116. data/lib/insika/edge_limiter.rb +268 -0
  117. data/lib/insika/egress_guard.rb +75 -0
  118. data/lib/insika/env_schema.rb +249 -0
  119. data/lib/insika/errors.rb +201 -0
  120. data/lib/insika/evals/assertions.rb +247 -0
  121. data/lib/insika/evals/baseline.rb +69 -0
  122. data/lib/insika/evals/golden.rb +172 -0
  123. data/lib/insika/evals/judge.rb +225 -0
  124. data/lib/insika/evals/pairwise.rb +178 -0
  125. data/lib/insika/evals/report.rb +115 -0
  126. data/lib/insika/evals/runner.rb +141 -0
  127. data/lib/insika/evals/transport.rb +178 -0
  128. data/lib/insika/event.rb +18 -0
  129. data/lib/insika/event_stream.rb +132 -0
  130. data/lib/insika/executor.rb +1995 -0
  131. data/lib/insika/frontmatter.rb +42 -0
  132. data/lib/insika/golden_store.rb +145 -0
  133. data/lib/insika/hooks.rb +48 -0
  134. data/lib/insika/http_client.rb +63 -0
  135. data/lib/insika/inbound_log.rb +84 -0
  136. data/lib/insika/llm_configurator.rb +99 -0
  137. data/lib/insika/llm_provider_store.rb +83 -0
  138. data/lib/insika/loop_detector.rb +143 -0
  139. data/lib/insika/mcp_http_client.rb +67 -0
  140. data/lib/insika/mcp_store.rb +115 -0
  141. data/lib/insika/mcp_tool_ingestor.rb +143 -0
  142. data/lib/insika/memory_store.rb +93 -0
  143. data/lib/insika/message_origin.rb +76 -0
  144. data/lib/insika/middleware.rb +36 -0
  145. data/lib/insika/model_policy.rb +52 -0
  146. data/lib/insika/model_resolver.rb +176 -0
  147. data/lib/insika/model_selection.rb +115 -0
  148. data/lib/insika/onboarding.rb +208 -0
  149. data/lib/insika/outbox_store.rb +166 -0
  150. data/lib/insika/overlay_tool_registry.rb +102 -0
  151. data/lib/insika/pack.rb +102 -0
  152. data/lib/insika/pack_importer.rb +123 -0
  153. data/lib/insika/pending_action_store.rb +120 -0
  154. data/lib/insika/plugin/loader.rb +356 -0
  155. data/lib/insika/plugin.rb +35 -0
  156. data/lib/insika/policy/engine.rb +83 -0
  157. data/lib/insika/policy/policy.rb +120 -0
  158. data/lib/insika/policy_registry.rb +23 -0
  159. data/lib/insika/profile_source.rb +143 -0
  160. data/lib/insika/prompt_catalog.rb +61 -0
  161. data/lib/insika/provider_error_classifier.rb +160 -0
  162. data/lib/insika/queue_policy.rb +167 -0
  163. data/lib/insika/recovery.rb +168 -0
  164. data/lib/insika/refinement/candidate.rb +159 -0
  165. data/lib/insika/refinement/evidence_collector.rb +371 -0
  166. data/lib/insika/refinement/gate.rb +234 -0
  167. data/lib/insika/refinement/panel.rb +222 -0
  168. data/lib/insika/refinement/proposer.rb +262 -0
  169. data/lib/insika/refinement_store.rb +295 -0
  170. data/lib/insika/registry.rb +59 -0
  171. data/lib/insika/reliability.rb +185 -0
  172. data/lib/insika/safety/config.rb +109 -0
  173. data/lib/insika/safety/detectors.rb +176 -0
  174. data/lib/insika/safety/factory.rb +102 -0
  175. data/lib/insika/safety/input_guardrail.rb +102 -0
  176. data/lib/insika/safety/moderator.rb +94 -0
  177. data/lib/insika/safety/output_filter.rb +79 -0
  178. data/lib/insika/safety/output_validator.rb +101 -0
  179. data/lib/insika/safety/safe_responses.rb +47 -0
  180. data/lib/insika/sandbox/boundary.rb +93 -0
  181. data/lib/insika/sandbox/docker.rb +74 -0
  182. data/lib/insika/sandbox/local.rb +33 -0
  183. data/lib/insika/sandbox/runner.rb +80 -0
  184. data/lib/insika/sandbox.rb +85 -0
  185. data/lib/insika/schema_guard.rb +147 -0
  186. data/lib/insika/secret_masking.rb +34 -0
  187. data/lib/insika/server/a2a/agent_card.rb +27 -0
  188. data/lib/insika/server/a2a/app.rb +112 -0
  189. data/lib/insika/server/a2a/client.rb +101 -0
  190. data/lib/insika/server/a2a/errors.rb +32 -0
  191. data/lib/insika/server/a2a/http.rb +42 -0
  192. data/lib/insika/server/a2a/message.rb +27 -0
  193. data/lib/insika/server/a2a/protocol.rb +45 -0
  194. data/lib/insika/server/a2a/remotes.rb +25 -0
  195. data/lib/insika/server/a2a/task_projection.rb +40 -0
  196. data/lib/insika/server/app.rb +1022 -0
  197. data/lib/insika/server/boot.rb +119 -0
  198. data/lib/insika/server/rack_app.rb +118 -0
  199. data/lib/insika/server/responses.rb +165 -0
  200. data/lib/insika/server/sse_body.rb +96 -0
  201. data/lib/insika/server/tenant_auth.rb +61 -0
  202. data/lib/insika/session_actor.rb +162 -0
  203. data/lib/insika/session_store.rb +143 -0
  204. data/lib/insika/settings_store.rb +154 -0
  205. data/lib/insika/shutdown.rb +125 -0
  206. data/lib/insika/skill_catalog.rb +220 -0
  207. data/lib/insika/skill_store.rb +127 -0
  208. data/lib/insika/steer_injector.rb +110 -0
  209. data/lib/insika/store.rb +52 -0
  210. data/lib/insika/stores/memory.rb +123 -0
  211. data/lib/insika/stores/sqlite.rb +183 -0
  212. data/lib/insika/studio/app.rb +1693 -0
  213. data/lib/insika/studio/assets/dist/application.css +1 -0
  214. data/lib/insika/studio/assets/dist/application.js +70 -0
  215. data/lib/insika/studio/forms.rb +335 -0
  216. data/lib/insika/studio/nav_icons.rb +31 -0
  217. data/lib/insika/studio/views/_message.erb +44 -0
  218. data/lib/insika/studio/views/agent_detail.erb +285 -0
  219. data/lib/insika/studio/views/agents.erb +63 -0
  220. data/lib/insika/studio/views/approvals.erb +41 -0
  221. data/lib/insika/studio/views/chats.erb +34 -0
  222. data/lib/insika/studio/views/evals.erb +83 -0
  223. data/lib/insika/studio/views/home.erb +72 -0
  224. data/lib/insika/studio/views/layout.erb +94 -0
  225. data/lib/insika/studio/views/login.erb +17 -0
  226. data/lib/insika/studio/views/mcp.erb +91 -0
  227. data/lib/insika/studio/views/not_found.erb +5 -0
  228. data/lib/insika/studio/views/playground.erb +47 -0
  229. data/lib/insika/studio/views/refinement.erb +234 -0
  230. data/lib/insika/studio/views/session.erb +137 -0
  231. data/lib/insika/studio/views/settings.erb +168 -0
  232. data/lib/insika/studio/views/skills.erb +141 -0
  233. data/lib/insika/studio/views/system_files.erb +65 -0
  234. data/lib/insika/studio/views/task.erb +105 -0
  235. data/lib/insika/studio/views/tasks.erb +33 -0
  236. data/lib/insika/studio/views/tool_edit.erb +107 -0
  237. data/lib/insika/studio/views/tools.erb +89 -0
  238. data/lib/insika/subagent_graph.rb +96 -0
  239. data/lib/insika/system_file_store.rb +96 -0
  240. data/lib/insika/task_actor.rb +128 -0
  241. data/lib/insika/task_store.rb +250 -0
  242. data/lib/insika/telemetry/pricing.rb +104 -0
  243. data/lib/insika/telemetry/recorder.rb +228 -0
  244. data/lib/insika/telemetry.rb +127 -0
  245. data/lib/insika/testing/store_contract.rb +270 -0
  246. data/lib/insika/tick.rb +122 -0
  247. data/lib/insika/token_estimator.rb +16 -0
  248. data/lib/insika/token_store.rb +168 -0
  249. data/lib/insika/tool_assembly.rb +140 -0
  250. data/lib/insika/tool_catalog.rb +89 -0
  251. data/lib/insika/tool_definition.rb +518 -0
  252. data/lib/insika/tool_envelope.rb +140 -0
  253. data/lib/insika/tool_manifest.rb +218 -0
  254. data/lib/insika/tool_output_compressor.rb +100 -0
  255. data/lib/insika/tool_registry.rb +21 -0
  256. data/lib/insika/tool_store.rb +135 -0
  257. data/lib/insika/tool_trace_store.rb +92 -0
  258. data/lib/insika/tools/a2a_remote.rb +48 -0
  259. data/lib/insika/tools/agent_enum.rb +68 -0
  260. data/lib/insika/tools/concurrency.rb +54 -0
  261. data/lib/insika/tools/data_defined_tool.rb +219 -0
  262. data/lib/insika/tools/load_skill.rb +99 -0
  263. data/lib/insika/tools/remember.rb +53 -0
  264. data/lib/insika/tools/stuck_signal.rb +44 -0
  265. data/lib/insika/tools/subagent.rb +75 -0
  266. data/lib/insika/tools/subagents.rb +77 -0
  267. data/lib/insika/tools/tool_search.rb +94 -0
  268. data/lib/insika/turn_output.rb +139 -0
  269. data/lib/insika/turn_state.rb +162 -0
  270. data/lib/insika/turn_timing.rb +56 -0
  271. data/lib/insika/usage_ledger.rb +47 -0
  272. data/lib/insika/version.rb +3 -1
  273. data/lib/insika/wiring/graph.rb +249 -0
  274. data/lib/insika/workflow.rb +185 -0
  275. data/lib/insika/workflow_registry.rb +33 -0
  276. data/lib/insika.rb +220 -4
  277. metadata +412 -8
data/docs/CHANNELS.md ADDED
@@ -0,0 +1,453 @@
1
+ ---
2
+ title: Channels
3
+ parent: Build an agent
4
+ nav_order: 6
5
+ permalink: /channels/
6
+ ---
7
+
8
+ # Channels
9
+
10
+ An agent is only useful where the people are. A **channel** is the seam between a
11
+ place people already talk — WhatsApp, Slack, a widget on your site, your own app —
12
+ and a turn of the engine.
13
+
14
+ A channel does exactly two things:
15
+
16
+ - **inbound**: normalize whatever the platform sent into a message for an agent;
17
+ - **outbound**: render the turn's answer back in that platform's shape.
18
+
19
+ Nothing else. A channel never decides which tools an agent may call, never edits a
20
+ prompt, never grants a capability. It may *refuse* a request (a bad signature); it
21
+ may not *widen* one. Everything a channel learns about the caller is data, and it
22
+ enters the prompt at the most cuttable priority, like any other untrusted input.
23
+
24
+ ## Which one is yours?
25
+
26
+ Two channels ship with the engine, and the choice between them is **where the
27
+ conversation happens**.
28
+
29
+ | | [**web widget**](#the-web-widget) | [**relay**](#the-relay-contract) |
30
+ |---|---|---|
31
+ | Where people talk to you | a panel on your own site | WhatsApp, Slack, your app — wherever you already are |
32
+ | Who talks to that platform | — (there is none) | **you**, with the integration you already have |
33
+ | Who runs the turn | the engine | the engine |
34
+ | What you have to build | nothing: one `<script>` tag | two HTTP calls |
35
+
36
+ **The web widget** is for a team with no messaging stack at all. Paste one tag on
37
+ your site and you have an agent — no backend of yours, no build step, no npm.
38
+
39
+ **The relay** is for a team that already owns its messaging stack — a WhatsApp BSP,
40
+ a Zendesk, a legacy Rails app with years of tuning in it. You keep every bit of
41
+ that. You POST the customer's message to the engine, the engine POSTs the answer
42
+ back to a URL you control, and your existing code does what it already does well.
43
+ Nothing about your platform integration has to move for you to adopt Insika.
44
+
45
+ They are not stages of the same path. Plenty of teams run both.
46
+
47
+ > **On Slack and WhatsApp "natively".** The engine does not speak either platform,
48
+ > and that is a decision rather than a gap. If your customers are on WhatsApp or
49
+ > Slack, the relay is the answer: you own the platform integration — the 24-hour
50
+ > window, templates, media, read receipts, formatting — and the engine owns the
51
+ > turn. That boundary is [the promise, not a limitation](#what-the-relay-does-not-do--on-purpose),
52
+ > and it is why the relay is permanent rather than a stepping stone.
53
+ >
54
+ > Adapters that make the engine speak a platform directly are specified and
55
+ > deliberately unbuilt. The seam they would plug into is real and proven — the two
56
+ > channels above are its opposite shapes, and a third would add `deliver` and
57
+ > nothing else — so this is a question of demand, not of design. If you need one,
58
+ > [open an issue](https://github.com/guizaols/insika/issues); you can also
59
+ > [write it yourself](#writing-your-own-channel) as a plugin, without forking.
60
+
61
+ ## The web widget
62
+
63
+ For a team with no messaging stack at all: **one `<script>` tag on your site and
64
+ you have an agent**. No backend of yours, no build step, no npm.
65
+
66
+ ```html
67
+ <script src="https://agents.example.com/channels/web/asset/widget.js"
68
+ data-agent="support" data-title="Ask us anything" defer></script>
69
+ ```
70
+
71
+ It renders a bubble in the corner, opens a panel, and streams the answer token by
72
+ token on the same connection it sent the message on. Everything it needs it reads
73
+ off its own tag — including the engine's address, derived from the script's own
74
+ `src`, so you never configure a host twice.
75
+
76
+ | Attribute | Required | What it does |
77
+ |---|---|---|
78
+ | `data-agent` | **yes** | which agent answers. Must be on `INSIKA_WIDGET_AGENTS` |
79
+ | `data-title` | — | the panel header (default `Chat`) |
80
+ | `data-placeholder` | — | the input's placeholder |
81
+ | `data-greeting` | — | a first message from the agent, shown without running a turn |
82
+
83
+ Theming is a block of CSS custom properties. Set them anywhere on the page:
84
+
85
+ ```css
86
+ :root {
87
+ --insika-accent: #0f766e; --insika-on-accent: #fff;
88
+ --insika-bg: #fff; --insika-fg: #111827;
89
+ --insika-muted: #f0fdfa; --insika-border: rgba(0,0,0,.12);
90
+ --insika-font: "Inter", system-ui, sans-serif;
91
+ --insika-offset: 20px; /* distance from the corner */
92
+ }
93
+ ```
94
+
95
+ A runnable page lives in
96
+ [`examples/web-widget/`](https://github.com/guizaols/insika/tree/main/examples/web-widget).
97
+
98
+ ### Setting up the widget
99
+
100
+ Two environment variables, and **both are the switch** — with either one missing the
101
+ channel is not mounted and every `/channels/web/*` route answers `404`:
102
+
103
+ ```bash
104
+ INSIKA_WIDGET_ORIGINS=https://shop.example,https://www.shop.example
105
+ INSIKA_WIDGET_AGENTS=support
106
+ ```
107
+
108
+ `INSIKA_WIDGET_ORIGINS` is an **exact-match** list of the origins allowed to embed
109
+ the widget. No wildcards, no subdomain matching, and no value that means "anyone" —
110
+ `https://shop.example` does not admit `https://a.shop.example`. Include every origin
111
+ your site actually serves from, `www` included.
112
+
113
+ `INSIKA_WIDGET_AGENTS` is the list of agents a visitor may address. An anonymous
114
+ browser addresses these and nothing else, so an internal agent in the same
115
+ deployment stays out of reach even if someone edits the `data-agent` in devtools.
116
+
117
+ ### A rate limit is required, not suggested
118
+
119
+ **The widget answers `503` until a chat rate limit is configured.** This is the one
120
+ place the engine refuses to run rather than warn, because a public endpoint with an
121
+ LLM behind it and no ceiling is an unmetered bill that arrives before anybody
122
+ notices.
123
+
124
+ Either source satisfies it — the platform default, in Studio → Settings:
125
+
126
+ ```json
127
+ { "edge": { "chat_rate_limit": 6, "chat_rate_window": 60 } }
128
+ ```
129
+
130
+ or a per-agent `limits.chat_rate_limit` on **every** agent in
131
+ `INSIKA_WIDGET_AGENTS`. The bucket is the minted session id, so one visitor cannot
132
+ spend another's allowance. `insika doctor` tells you which half is missing.
133
+
134
+ ### The three routes
135
+
136
+ You never call these yourself — the widget does — but they are the contract, and
137
+ anything can speak them.
138
+
139
+ ```
140
+ POST /channels/web/sessions -> 201 {"session_id": "web:8f3c…"}
141
+ POST /channels/web/messages -> 200 text/event-stream
142
+ GET /channels/web/asset/widget.js -> the script
143
+ ```
144
+
145
+ ```jsonc
146
+ POST /channels/web/messages
147
+ Content-Type: application/json
148
+
149
+ { "agent": "support", "session_id": "web:8f3c…", "message": "cadê meu pedido?" }
150
+ ```
151
+
152
+ The reply is SSE on that same connection — four frame types, and an unknown one is
153
+ safe to ignore:
154
+
155
+ ```
156
+ event: delta data: {"delta":"Seu pedido "} the answer, token by token
157
+ event: working data: {"name":"order_status"} a tool is running
158
+ event: done data: {} the turn ended
159
+ event: error data: {"message":"…"} it ended badly
160
+ ```
161
+
162
+ **The engine issues the session id and the client never proposes one.** `POST
163
+ /messages` with an id nobody minted is a `404`, never a new conversation: on an
164
+ anonymous endpoint, create-on-write means anyone who guesses an id can read someone
165
+ else's chat. The widget keeps the id it was given in `localStorage`, so a returning
166
+ visitor continues the same conversation.
167
+
168
+ ### What the widget does not do
169
+
170
+ File upload, typing indicators, history across devices, and i18n of its own chrome
171
+ (the four words on the buttons). It also never retries a message POST: that request
172
+ runs a turn, so re-sending it costs a second LLM call and can put a second answer in
173
+ front of the customer. A dropped stream shows what arrived and lets the person ask
174
+ again.
175
+
176
+ ## Relay or the drop-in API?
177
+
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
180
+ whole turn and read the answer off it — or the relay, where the engine acks in
181
+ milliseconds and POSTs the answer to you when it exists.
182
+
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.
190
+
191
+ | | drop-in `/v1/responses` | relay |
192
+ |---|---|---|
193
+ | What the customer receives | one message, at the end | one message, at the end |
194
+ | Your app's request | held open for the whole turn (seconds) | acked in **milliseconds** |
195
+ | A turn that outlives your HTTP timeout | your problem | already handled — the answer arrives later |
196
+ | Retry on a failed handover | yours to build | the engine's outbox, bounded, at-most-once |
197
+ | Three fragments typed in a row | three turns, three replies | **one turn, one reply** (with `queue_mode`) |
198
+ | Your platform code | unchanged | unchanged |
199
+
200
+ That last row is the one that cannot be had the other way. `/v1/responses` answers
201
+ the request it was given, so a message that arrives while a turn is running is a
202
+ second turn — the engine has no way to tell you "this joined the previous one". The
203
+ 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)
205
+ is only reachable from here.
206
+
207
+ **Measured, so you can judge it rather than take our word:** same agent, same
208
+ conversations, one local deployment.
209
+
210
+ ```
211
+ drop-in relay
212
+ greeting 2.3s 2.9s (ack 11ms)
213
+ catalog (4 tool calls) 6.8–12.3s 9.4s (ack 2ms)
214
+ 3 fragments 3 turns/3 replies 1 turn/1 reply, with queue_mode
215
+ ```
216
+
217
+ The spread on the catalog turn is the tool retrying, not the transport — the relay
218
+ adds one HTTP POST, not seconds. Your numbers will differ; the *shapes* are what
219
+ transfer.
220
+
221
+ **Pick the drop-in** if you already have it working and none of the rows above bite.
222
+ **Pick the relay** if you want the queue, or if holding a connection for the length
223
+ of a turn is awkward where your app runs.
224
+
225
+ ## What the relay does not do — on purpose
226
+
227
+ Platform semantics stay with you:
228
+
229
+ - the 24-hour customer-service window and template fallback,
230
+ - template approval and template sending,
231
+ - media upload and download,
232
+ - typing indicators and read receipts,
233
+ - how a markdown reply becomes WhatsApp formatting.
234
+
235
+ The engine sends **text and identity**. That is the promise, not the limitation:
236
+ it is what lets you keep an integration you have already tuned, and it is why
237
+ nothing here expires. A relay that starts growing template logic has stopped being
238
+ a relay; the place for platform semantics is the code that already has them —
239
+ yours — or a [channel of your own](#writing-your-own-channel).
240
+
241
+ ## The relay contract
242
+
243
+ Two routes and an envelope.
244
+
245
+ ```
246
+ you ──POST /channels/relay/events──▶ engine acked now, never the reply
247
+ you ◀──POST <your deliver_url>───── engine the reply, when there is one
248
+ ```
249
+
250
+ ### Inbound
251
+
252
+ ```jsonc
253
+ POST /channels/relay/events
254
+ Authorization: Bearer <INSIKA_RELAY_TOKEN>
255
+ Content-Type: application/json
256
+
257
+ {
258
+ "agent": "support", // required — which agent answers
259
+ "external_id": "5511999998888", // required — YOUR key for this conversation
260
+ "message": "queria saber do pedido", // required
261
+ "event_id": "wamid.HBg…", // optional but strongly recommended (dedup)
262
+ "vars": { "store": "demo-store" } // optional — session vars on first contact
263
+ }
264
+ ```
265
+
266
+ The engine acks immediately and never streams the answer here. Four possible
267
+ answers, and **they are four different facts**:
268
+
269
+ | Status | Body | What happened | What you must do |
270
+ |---|---|---|---|
271
+ | `202` | `{"task_id": "…"}` | a turn is running | wait for the delivery |
272
+ | `200` | `{"task_id": "…", "duplicate": true}` | we already ran this `event_id` | nothing — the original reply is on its way |
273
+ | `200` | `{"task_id": "…", "merged": true}` | it joined a turn still at the door | nothing — the answer belongs to `task_id` |
274
+ | `200` | `{"task_id": "…", "steered": true}` | it was appended to a turn already running | nothing — same |
275
+ | `422` | `{"error": …}` | the envelope is malformed | fix and resend |
276
+ | `401` / `503` | `{"error": …}` | bad token / channel not configured | fix the credential |
277
+
278
+ **A consumer that treats the three `200`s like a `202` sends the customer the same
279
+ answer two or three times.** That is the one contract mistake that is visible to
280
+ the end user, so it is worth a line of code: only deliver for a `202`.
281
+
282
+ `merged` and `steered` come from the [inbound queue](/agents/#queue_mode--when-a-message-arrives-while-the-agent-is-busy)
283
+ (`limits[:queue_mode]`). They only ever occur if you turned that on for the agent;
284
+ with the default `followup` you will only see `202` and `duplicate`.
285
+
286
+ ### Outbound
287
+
288
+ One POST per reply, to the URL you configured:
289
+
290
+ ```jsonc
291
+ POST <INSIKA_RELAY_DELIVER_URL>
292
+ Authorization: Bearer <INSIKA_RELAY_DELIVER_TOKEN> // omitted if unset
293
+ X-Insika-Delivery: 0f2c… // stable idempotency key
294
+ Content-Type: application/json
295
+
296
+ {
297
+ "external_id": "5511999998888",
298
+ "session_id": "relay:5511999998888",
299
+ "task_id": "…",
300
+ "content": "Seu pedido saiu para entrega hoje 😊"
301
+ }
302
+ ```
303
+
304
+ Any `2xx` means delivered. Anything else is retried, up to three attempts with a
305
+ short backoff, and then the delivery is marked `failed` and stops.
306
+
307
+ `content` is the turn's **answer** — one message, whole. The model's narration on
308
+ the way to an answer ("vou verificar o cardápio…") does not come through here; it
309
+ 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
311
+ forward `content` straight to the customer.
312
+
313
+ ### Deduplication
314
+
315
+ Send `event_id` and a retried webhook costs you nothing: the engine recognizes the
316
+ id inside a 24-hour window, answers `duplicate: true`, and runs no turn. Without
317
+ it, every retry on your side is a second LLM turn (that you pay for) and a second
318
+ message (that the customer reads).
319
+
320
+ The engine will not invent dedup from a content hash — two customers typing "oi"
321
+ a second apart are not a duplicate. No `event_id` means at-least-once turns, and
322
+ that is stated rather than hidden.
323
+
324
+ ### Delivery guarantee: at-most-once
325
+
326
+ The reply is written durably when the turn commits, then **claimed** before the
327
+ HTTP call. If the process dies between the claim and the POST, that delivery is
328
+ lost; it is never duplicated. On boot, replies that were recorded but never
329
+ claimed are re-dispatched.
330
+
331
+ This is deliberate and it is the same guarantee the engine's async-subagent
332
+ delivery has. If you need at-least-once instead, the honest place to build it is
333
+ your side: you already have the customer's conversation, and `GET /v1/tasks/:id`
334
+ tells you the turn's terminal state.
335
+
336
+ 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.
338
+
339
+ ## Setting up the relay
340
+
341
+ Three environment variables on the engine:
342
+
343
+ ```bash
344
+ INSIKA_RELAY_TOKEN=<a long random secret> # the switch AND the credential
345
+ INSIKA_RELAY_DELIVER_URL=https://you.example/insika/deliver
346
+ INSIKA_RELAY_DELIVER_TOKEN=<another secret> # optional; what we send to you
347
+ ```
348
+
349
+ `INSIKA_RELAY_TOKEN` is the switch: without it the channel is not mounted and
350
+ `/channels/relay/events` answers `404`. There is no way to expose this route
351
+ without a credential — a public inbound route with an LLM behind it is a money
352
+ faucet, so it fails closed by construction.
353
+
354
+ The delivery POST goes through the same [egress guard](/security/#egress-the-ssrf-boundary) as
355
+ data-tools: **https only**, and private/loopback destinations blocked. For local
356
+ development, where your consumer is on `localhost`:
357
+
358
+ ```bash
359
+ INSIKA_EGRESS_ALLOW_HTTP=1
360
+ INSIKA_EGRESS_ALLOW_PRIVATE=1
361
+ ```
362
+
363
+ Run `insika doctor` after configuring — it refuses to let you have half of it
364
+ (a token with no URL accepts turns it can never answer; a URL with no token mounts
365
+ nothing at all).
366
+
367
+ A runnable consumer in ~40 lines lives in
368
+ [`examples/relay-channel/`](https://github.com/guizaols/insika/tree/main/examples/relay-channel).
369
+
370
+ ## Sessions
371
+
372
+ The engine mints the session id: `relay:<your external_id>`. Namespacing is not
373
+ decoration — it stops a Slack channel id from colliding with a phone number, and
374
+ it means an id from one channel can never read another channel's conversation.
375
+
376
+ Your `vars` ride along on first contact and become session vars, except for
377
+ `channel` and `external_id`, which the engine always owns. A caller cannot
378
+ redirect its own conversation.
379
+
380
+ ## Writing your own channel
381
+
382
+ 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,
384
+ and it mounts under `/channels/<id>/`.
385
+
386
+ Two members are always there; the rest of the object decides which shape you get.
387
+
388
+ ```ruby
389
+ class MyChannel
390
+ def id = "mine"
391
+
392
+ # -> :ok | :unauthorized | :disabled. Never open by omission — a channel with
393
+ # no `authenticate` at all is refused with 503 rather than defaulted open.
394
+ def authenticate(req) = ...
395
+
396
+ # Rack request + parsed body -> the fields the engine turns into a turn.
397
+ def parse(req, body:) = ...
398
+ end
399
+ ```
400
+
401
+ **Shape B** (the platform calls you back later) adds `deliver`, and that alone
402
+ mounts `POST /channels/<id>/events`:
403
+
404
+ ```ruby
405
+ def parse(req, body:)
406
+ { agent: …, external_id: …, message: …, event_id: …, vars: {} }
407
+ end
408
+
409
+ def session_id_for(external_id) = "#{id}:#{external_id}"
410
+
411
+ # Hand ONE reply to the platform. -> HTTP status.
412
+ # Raise Insika::DeliveryError when the request could not be made at all.
413
+ def deliver(payload, to:, delivery_id: nil) = ...
414
+ ```
415
+
416
+ **Shape A** (the reply rides the request's own connection) adds `frame_for`, which
417
+ mounts `POST /channels/<id>/messages`, plus whichever of these it wants:
418
+
419
+ ```ruby
420
+ def parse(req, body:) = { agent: …, session_id: …, message: … }
421
+
422
+ # Turn Event -> an SSE frame | nil for an event with no counterpart.
423
+ def frame_for(event) = ...
424
+
425
+ # Optional: mounts POST /channels/<id>/sessions. The engine issues the id.
426
+ def mint_session_id = "#{id}:#{SecureRandom.hex(16)}"
427
+
428
+ # Optional: mounts GET /channels/<id>/asset/:f. A CLOSED map of names, never a
429
+ # path — anything resolving a filesystem path from a URL is a traversal.
430
+ def asset(name) = { content_type: …, body: …, etag: …, cache_control: … }
431
+
432
+ # Optional: the CORS headers for this origin, or none.
433
+ def cors_headers(origin) = ...
434
+ ```
435
+
436
+ Everything else is shared — the outbox, the at-most-once claim, the bounded retry,
437
+ the inbound dedup and the queue. A channel that needs to branch anywhere but
438
+ `deliver` is a sign the seam is wrong, not that the channel is special.
439
+
440
+ ## Observability
441
+
442
+ Each delivery emits `:channel_delivered` on the event stream —
443
+ `{channel, outbox_id, status, attempts, error}` — so a failed handover is visible
444
+ without reading the store. Inbound is already visible: the turn's persisted
445
+ command carries `transport: "channel:<id>"`.
446
+
447
+ ## See also
448
+
449
+ - [Security](/security/) — the tokens, the egress guard, and why the rate limit
450
+ matters for anything public.
451
+ - [Agents](/agents/) — `limits[:queue_mode]`, which is what produces `merged` and
452
+ `steered`.
453
+ - [Observability](/observability/) — the event stream and OpenTelemetry.
data/docs/CONTEXT.md ADDED
@@ -0,0 +1,117 @@
1
+ ---
2
+ title: Context
3
+ parent: Build an agent
4
+ nav_order: 4
5
+ permalink: /context/
6
+ ---
7
+
8
+ # Context
9
+
10
+ Every turn, the **Context Builder** assembles the model's prompt from a fixed set
11
+ of **providers**, estimates its token cost, and evicts the lowest-priority
12
+ non-pinned fragments to fit the agent's `context_budget`. The Executor never
13
+ builds the prompt itself — it receives a finished context package. Providers run
14
+ in parallel, each with a short per-provider timeout, and an *optional* provider
15
+ that fails or times out is dropped with a warning rather than failing the turn.
16
+
17
+ ## What enters a turn's prompt
18
+
19
+ Providers are chosen by a double gate (the provider opts in for the profile **and**
20
+ the agent's `context_providers` allowlist permits it), then assembled by priority
21
+ into a deterministic prompt:
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).
36
+
37
+ ## Budget and eviction — the actual "compaction"
38
+
39
+ - The cap is `profile.limits[:context_budget]`, **default 8000 tokens**.
40
+ - To fit the budget, the builder cuts **non-pinned** fragments
41
+ 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
43
+ identity is never truncated**.
44
+ - A **pinned** fragment (the identity) that *alone* exceeds the budget raises an
45
+ error — the turn fails rather than shipping a truncated identity.
46
+
47
+ > ⚠️ **The `context_budget` gotcha.** A rich system prompt can run to tens of
48
+ > thousands of tokens — far past the 8000 default. If a newly created agent
49
+ > returns empty turns, raise `context_budget` (e.g. to `60000`) before looking
50
+ > anywhere else. See [Agents](AGENTS.md#default-limits).
51
+
52
+ ### Compaction is not wired — except the mechanical dedupe
53
+
54
+ There is a settings stub for LLM-summarization compaction (`enabled: false`,
55
+ `keep_last`, a reserved utility-model slot), but **nothing consumes it today**
56
+ — and the Studio no longer shows a form for it, so the setting cannot be
57
+ switched on by accident. Size is managed purely by hard budget eviction.
58
+ Do not rely on compaction to shrink a bloated agent: tune `context_budget` and
59
+ keep the identity lean.
60
+
61
+ One cheap half **is** wired, opt-in per agent: `tool_output_compression` (DSL
62
+ `tool_output_compression`, or `"tool_output_compression": true` in the pack).
63
+ When on, byte-identical repeated **tool results** in the replayed history
64
+ collapse to a compact back-reference (the first occurrence stays full, with a
65
+ one-line summary) — no LLM involved. It changes what the model sees, so it is
66
+ never a default: an older detail is only in the first occurrence, and a model
67
+ that wants it re-calls the tool. Reach for it when a tool keeps returning the
68
+ same body (a catalog page, a status) and history is the fragment blowing the
69
+ budget first.
70
+
71
+ > The **Studio session screen** shows what the builder assembled per turn —
72
+ > tokens per category (identity, history, memory, …), the tools-schema estimate
73
+ > and the budget verdict (`used / cap`, evicted sources). Counts only, never
74
+ > fragment content.
75
+
76
+ ## Memory
77
+
78
+ With `memory` enabled, an agent gains a built-in `remember` tool for durable
79
+ facts, and those facts (plus recent notes) are injected back into the prompt on
80
+ 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
82
+ conversation; memory is the small set of facts that should outlive any single
83
+ conversation. Facts and notes are editable from the Studio agent page. See
84
+ [`examples/memory/`](https://github.com/guizaols/insika/tree/main/examples/memory/) for a runnable cross-session example.
85
+
86
+ ## The provider prefix cache
87
+
88
+ Two distinct caching mechanisms — don't conflate them:
89
+
90
+ - **Automatic server-side prefix cache.** Some providers prefix-cache a stable
91
+ 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.
94
+ Anything that injects volatile content high in the system block breaks the cache.
95
+ - **Manual cache breakpoints (opt-in).** With `prompt_caching` on **and** a
96
+ provider that supports explicit cache control, the builder sets one cache
97
+ breakpoint at the end of the system block. Only enable this for a byte-stable
98
+ system — a volatile system turns every turn into a paid cache *write*.
99
+
100
+ Cache accounting surfaces as `cached_tokens` (reads) and `cache_creation_tokens`
101
+ (writes), visible in telemetry and the Studio tokens chip.
102
+
103
+ ## The volume
104
+
105
+ Agents, prompts, skills, and tools are **data in SQLite**, not files on a volume.
106
+ A deploy swaps the image but does **not** rewrite the database — so changing a
107
+ committed file changes nothing on a running box. Updates happen at runtime through
108
+ the Studio, the API, or the DSL. The durable volume persists only the database
109
+ (with optional continuous replication — see [Deploy](DEPLOY.md)). This is why the
110
+ instinct to "commit a file and redeploy" fails for agents, skills, and tools alike.
111
+
112
+ ## See also
113
+
114
+ - [Agents](AGENTS.md) — `context_budget` and the other limits.
115
+ - [Skills](SKILLS.md) — how the `<available_skills>` list is built and budgeted.
116
+ - [Tools](TOOLS.md) — deferred tools and `tool_search`.
117
+ - [Architecture](ARCHITECTURE.md) — where the Context Builder sits in a turn.