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/DEPLOY.md ADDED
@@ -0,0 +1,354 @@
1
+ ---
2
+ title: Deploy
3
+ parent: Ship it
4
+ nav_order: 3
5
+ permalink: /deploy/
6
+ ---
7
+
8
+ # Deploy
9
+
10
+ How to run the engine in a container (Railway today, Kubernetes later) and how to
11
+ measure performance and load.
12
+
13
+ > **This page deploys the repo, not the gem.** The reference deployment is a
14
+ > checkout of this same repository — `config/` is its composition root, and the
15
+ > `Gemfile` consumes the engine through the gemspec. An adopter deploys
16
+ > `gem install insika` (or a `Gemfile` line) instead.
17
+
18
+ ## Image (Docker)
19
+
20
+ The `Dockerfile` (multi-stage, YJIT on) serves `config.ru` under Falcon. The Studio
21
+ ships with its `dist/` built and vendored — **no Node in the build**. The backend
22
+ is durable SQLite (WAL) at `INSIKA_DB`; mount a volume and point it inside.
23
+
24
+ ```bash
25
+ docker build -t insika .
26
+ docker run -p 9292:9292 -v insika-data:/data \
27
+ -e DEEPSEEK_API_KEY=sk-... \
28
+ -e OPENCLAW_GATEWAY_TOKEN=change-me \
29
+ insika
30
+ curl localhost:9292/up # {"status":"ok"}
31
+ ```
32
+
33
+ ## The process model
34
+
35
+ The image boots **N Falcon worker processes over one SQLite file**
36
+ (`WEB_CONCURRENCY`, default 1). That number is a **contract input, not a tuning
37
+ knob**: it decides which engine semantics hold cluster-wide and which are
38
+ per-worker. The default is 1 because the per-worker semantics are the product
39
+ — the queue modes (`collect`/`steer`) and FIFO ordering need one
40
+ session actor; raise N only with sticky routing per session in front. The
41
+ contract:
42
+
43
+ > Everything here describes N workers of **one** deployment — one graph, replicated.
44
+ > N *graphs* inside one process is a different contract, and it is
45
+ > [Embedding](EMBEDDING.md): there each graph owns its own store and credentials,
46
+ > and the host — not the engine — installs the drain.
47
+
48
+ 1. **N workers share one SQLite store.** Everything durable — sessions, tasks,
49
+ checkpoints, outbox, delegations — is cross-process state. Any status
50
+ transition that hands work to "whoever gets there first" goes through a
51
+ transactional claim (`Store#transaction`); a bare read-check-write on a
52
+ shared status field is a bug by definition.
53
+ 2. **A session's live semantics are per-worker.** Per-session FIFO ordering,
54
+ `steer`, `interrupt`, `pause`/`cancel` and the SSE watch operate on the
55
+ worker that holds the session's actor. The engine does **not** promise them
56
+ across workers. A deploy that needs those semantics for a session must route
57
+ that session's traffic to one worker (sticky routing) — or accept per-worker
58
+ best-effort.
59
+ 3. **Recovery is part of boot, in every wiring.** Every worker boots through
60
+ `Server::Boot`, which runs recovery **before the listen**. The per-record
61
+ sweeps (undelivered outbox records, undelivered delegation results) run in
62
+ every worker — each record carries its own transactional claim (item 1), so
63
+ at-most-once holds however many workers sweep. The **task sweep** runs
64
+ **once per boot generation**: the sweep's "orphaned `:running`" test cannot
65
+ see a fiber living in a *sibling* process, so the first worker to claim
66
+ `INSIKA_BOOT_ID` (one id per container start, exported by
67
+ `deploy/entrypoint.sh`) sweeps and the rest skip. A worker respawned
68
+ mid-generation skips too — sweeping then would steal its siblings' live
69
+ turns; its own orphans wait for the next generation (the next deploy).
70
+ Without `INSIKA_BOOT_ID` (single-process runs) every boot sweeps.
71
+ 4. **Shutdown is a drain, not a kill.** On SIGTERM (or SIGINT) a worker stops
72
+ accepting new turns — a turn that arrives mid-drain stays `:queued` and the
73
+ next boot's recovery replays it — and waits up to `INSIKA_DRAIN_TIMEOUT`
74
+ (default **20s**) for the in-flight ones. A second signal skips the wait.
75
+ Whatever the deadline abandons dies `:running`, and item 3 picks it up at the
76
+ next boot. The layers above must grant the time: `deploy/entrypoint.sh`
77
+ passes Falcon `--graceful-stop` = drain + 5 (Falcon's own default is 1s),
78
+ and the platform's SIGTERM→SIGKILL buffer must be ≥ drain + 10. **On Railway
79
+ that buffer defaults to 0** — SIGKILL right after SIGTERM, which cancels the
80
+ whole drain — so set `RAILWAY_DEPLOYMENT_DRAINING_SECONDS=30` on the
81
+ service.
82
+ 5. **A periodic tick closes the gap between boots.** Serving workers run a tick
83
+ every `INSIKA_TICK_INTERVAL` (default **60s**, `0` disables) as a child of
84
+ the turn supervisor: it re-drives any outbox record left `:pending` (each
85
+ carries its own claim — every worker may drain), and sweeps orphaned tasks
86
+ **without waiting for a deploy** — which is what recovers the orphans of a
87
+ worker respawned mid-generation (item 3's hole). One worker per window
88
+ sweeps (a single transactional claim), and only `:queued`/`:running` tasks
89
+ untouched for `INSIKA_TICK_STALE_AFTER` (default **900s**) are candidates:
90
+ a live turn is bounded by `turn_timeout`, so anything older cannot be alive.
91
+ **If you raise `turn_timeout` past it, raise `INSIKA_TICK_STALE_AFTER` too** —
92
+ the threshold must exceed the largest `turn_timeout` of the deployment.
93
+ `:waiting`/`:paused` tasks are idle by nature and stay boot recovery's.
94
+
95
+ `deploy/entrypoint.sh` sets `WEB_CONCURRENCY` next to a pointer to this section;
96
+ this section is the single source of truth for what changing it means.
97
+
98
+ ## Environment variables
99
+
100
+ | Env | Default | Effect |
101
+ |-----|---------|--------|
102
+ | `INSIKA_DB` | `/data/insika.db` (in the image) | durable SQLite path (**mount a volume!**) |
103
+ | `PORT` | `9292` | HTTP bind port |
104
+ | `WEB_CONCURRENCY` | `1` | number of Falcon worker processes — a contract input, see [The process model](#the-process-model) |
105
+ | `INSIKA_BOOT_ID` | set by `deploy/entrypoint.sh` | boot generation id; the recovery **task sweep** runs once per id (process model, item 3). Unset = every boot sweeps (single-process default) |
106
+ | `INSIKA_DRAIN_TIMEOUT` | `20` | seconds a stopping worker waits for in-flight turns before abandoning them to the next boot's recovery (process model, item 4). The entrypoint sizes Falcon's `--graceful-stop` from it; on Railway also set `RAILWAY_DEPLOYMENT_DRAINING_SECONDS` ≥ drain + 10 |
107
+ | `INSIKA_TICK_INTERVAL` | `60` | seconds between tick passes — outbox drain + stale recovery sweep (process model, item 5). `0` disables |
108
+ | `INSIKA_TICK_STALE_AFTER` | `900` | seconds a `:queued`/`:running` task must sit untouched before the tick sweeps it. Must exceed the largest `turn_timeout` of the deployment |
109
+ | `OPENCLAW_GATEWAY_TOKEN` | falls back to `ADMIN_TOKEN` | Bearer for `/v1/responses` and `/v1/agents` (the API contract) |
110
+ | `ADMIN_TOKEN` | `local-demo` | login token for `/studio` (**change in production**) |
111
+ | `DEEPSEEK_API_KEY` | — | provider key. **Without it the engine still boots** (`/up` green), but turns fail until it is configured (env or Studio → LLM providers) — cloud resilience |
112
+ | `DEEPSEEK_MODEL` | `deepseek-v4-flash` | model |
113
+ | `CONSUMER_INTERNAL_URL` | — | base URL for data-tools calling back a consumer's internal API (see below) |
114
+ | `INSIKA_EGRESS_HOSTS` | — | outbound host allowlist (SSRF guard) |
115
+ | `INSIKA_EGRESS_ALLOW_HTTP` / `_ALLOW_PRIVATE` | off | for `http`/loopback callbacks only (**never in cloud**) |
116
+ | `INSIKA_RELAY_TOKEN` | — | **mounts the relay channel** at `POST /channels/relay/events`, and is the Bearer it requires. Empty = the route does not exist (`404`). See [Channels](CHANNELS.md) |
117
+ | `INSIKA_RELAY_DELIVER_URL` | — | your callback; the engine POSTs each reply there. Goes through the egress guard |
118
+ | `INSIKA_RELAY_DELIVER_TOKEN` | — | Bearer the engine sends **to** your callback (optional) |
119
+ | `INSIKA_WIDGET_ORIGINS` | — | exact-match origins allowed to embed the [web widget](CHANNELS.md#the-web-widget), comma-separated. No wildcards. **Half the switch**: with `INSIKA_WIDGET_AGENTS` unset, nothing is mounted (`404`) |
120
+ | `INSIKA_WIDGET_AGENTS` | — | agent ids a widget visitor may address, comma-separated. The other half of the switch. **A chat rate limit is also required** or the widget answers `503` |
121
+ | `LITESTREAM_REPLICA_URL` | — | **enables Litestream** (backup/DR). Empty = disabled (default). See below |
122
+ | `LITESTREAM_ENDPOINT` | — | S3-compatible endpoint (R2/MinIO). Empty = AWS S3 |
123
+ | `LITESTREAM_REGION` | — | bucket region (AWS: `us-east-1`; R2: `auto`) |
124
+ | `LITESTREAM_ACCESS_KEY_ID` / `LITESTREAM_SECRET_ACCESS_KEY` | — | bucket credentials (read natively by Litestream) |
125
+
126
+ > **Renamed from `HARNESS_*` → `INSIKA_*`.** Every engine variable now uses the
127
+ > `INSIKA_` prefix. The old `HARNESS_*` names are still honored as deprecated aliases
128
+ > — set either one and the engine reads it, logging a one-line deprecation notice at
129
+ > boot (`insika doctor` reports it too). Migrate at your convenience; the legacy names
130
+ > will be dropped in a future release.
131
+
132
+ > **The database filename changed too** — the image now defaults to
133
+ > `INSIKA_DB=/data/insika.db` (it was `/data/harness.db`). **Existing volumes are
134
+ > adopted automatically:** the container entrypoint renames the old file — with its
135
+ > `-wal`/`-shm` siblings, before anything opens it — when the configured path does
136
+ > not exist yet. Nothing to run by hand, no data lost, and a no-op from the second
137
+ > boot on. To keep the old filename instead, point `INSIKA_DB` at it: the variable
138
+ > is the knob, the image only picks a default. (The adoption lives in
139
+ > `deploy/entrypoint.sh`, not in the engine — it is deploy baggage, not a runtime
140
+ > behavior.)
141
+
142
+ ### Tokens & rotation (keep the two separate!)
143
+
144
+ There are **two** secrets with distinct purposes — in production use **different**
145
+ values (the API token falling back to `ADMIN_TOKEN` is a dev convenience only):
146
+
147
+ - **`ADMIN_TOKEN`** — the `/studio` login (cookie auth). This is the **operator**
148
+ surface (just you). Rotating it is **safe and independent**: change it, redeploy,
149
+ log in with the new value. It does not affect any API consumer.
150
+ - **`OPENCLAW_GATEWAY_TOKEN`** — the Bearer for `/v1/responses` and `/v1/agents`.
151
+ This is the **contract with your API consumers**. Rotating it means **changing
152
+ both sides together** (or the integration breaks): update the runtime var **and**
153
+ each consumer's token in the same step.
154
+
155
+ Generate a strong token: `ruby -rsecurerandom -e 'puts SecureRandom.hex(24)'`.
156
+
157
+ ### Strict config and `insika doctor`
158
+
159
+ Config discipline that **rejects unknown keys — no silent schema tolerance**. Two
160
+ parts:
161
+
162
+ **1. Boot gate.** On boot, the engine validates the environment against a schema of
163
+ known keys (`Insika::EnvSchema`): a wrong type (`INSIKA_PORT=abc`) or an
164
+ **unknown key in the `INSIKA_` namespace** (a typo like `INSIKA_EGRES_ALLOW_HTTP`
165
+ the runtime would silently ignore). By **default it only warns** and boots anyway
166
+ (*last-known-good* — a rotated key or a typo never takes the whole service down).
167
+ To **refuse boot** on any finding, set `INSIKA_CONFIG_STRICT=1`. Unknown-key
168
+ detection is scoped to the `INSIKA_` prefix; the shared `OPENCLAW_`, `LITESTREAM_`,
169
+ and `OTEL_` namespaces are never flagged.
170
+
171
+ **2. `bin/insika doctor` — on-demand diagnostics.** Reads the **same** durable
172
+ backend the server uses (`INSIKA_DB`) without booting the whole app (no provider,
173
+ no seed) — safe to run against a production volume:
174
+
175
+ ```bash
176
+ insika doctor # colored report; exits != 0 on any error
177
+ insika doctor --json # machine-readable (CI / monitoring)
178
+ insika doctor --fix # applies the safe autofixes and re-diagnoses
179
+ insika env # lists known keys + current values (secrets masked)
180
+ ```
181
+
182
+ Checks: env (the schema above), settings schema version (a pending migration →
183
+ `--fix` applies it), a missing platform `default_model` (`--fix` seeds it from
184
+ `DEEPSEEK_MODEL`), durable vs ephemeral backend, LLM provider configured,
185
+ `ADMIN_TOKEN` set, data-tool definitions still valid, **prompt files that hold
186
+ text rather than a serialized object** (a file whose content is a stringified Hash
187
+ serves a mangled prompt on every turn while looking perfectly healthy — present,
188
+ non-empty, and the agent still answers), and **skill drift** — a shared skill whose
189
+ body names one store, a prompt file routing to a skill the agent cannot load, a broken
190
+ companion pair, a stale `eager:` key (see
191
+ [Skills](SKILLS.md#drift-guards)). Settings-schema migrations are **explicit**
192
+ — no Studio save silently reinterprets old-shape data.
193
+
194
+ ### Data-tool callbacks to a backend — via a tunnel
195
+
196
+ Data-tools call back a consumer's internal HTTP API. With the engine **in the
197
+ cloud** and your backend **on your machine** (`:3000`), expose it over a public
198
+ `https` tunnel and point the engine at it:
199
+
200
+ ```bash
201
+ # in the tool/manifest: base_url = {{env.CONSUMER_INTERNAL_URL}}
202
+ CONSUMER_INTERNAL_URL=https://your-tunnel.example.dev
203
+ INSIKA_EGRESS_HOSTS=your-tunnel.example.dev
204
+ ```
205
+
206
+ Because the tunnel is **public `https`**, the strict egress guard (the default)
207
+ **already allows it** — you do **not** need `ALLOW_HTTP`/`ALLOW_PRIVATE` (those are
208
+ only for a fully-local loop). Restricting `INSIKA_EGRESS_HOSTS` to the tunnel host
209
+ is the secure posture. See [Security](SECURITY.md#egress-the-ssrf-boundary).
210
+
211
+ ## Railway
212
+
213
+ `railway.json` already configures the Dockerfile builder, `startCommand`, the `/up`
214
+ healthcheck, and a restart policy.
215
+
216
+ 1. Create the project/service from this repo (builder = Dockerfile).
217
+ 2. **Volume**: mount it at `/data` (the default `INSIKA_DB` points there) —
218
+ without a volume, SQLite is ephemeral and recovery resumes nothing after a
219
+ redeploy.
220
+ 3. **Vars**: `DEEPSEEK_API_KEY`, `OPENCLAW_GATEWAY_TOKEN`, `CONSUMER_INTERNAL_URL`,
221
+ `INSIKA_EGRESS_HOSTS` (and `WEB_CONCURRENCY` to match your plan/CPU).
222
+ 4. The healthcheck hits `/up`.
223
+ 5. Point your consumer at the service's public URL, with a matching API token
224
+ (see [RUNNING-LOCAL.md](RUNNING-LOCAL.md)).
225
+
226
+ ## Backup / DR — Litestream (opt-in, configurable)
227
+
228
+ A single volume is the **one point of total loss** between a pilot and production
229
+ (disk corruption/loss = goodbye conversations + config). Litestream does
230
+ **continuous replication** of `insika.db` (its WAL) to an S3/R2 bucket, without
231
+ changing databases and **without a line of Ruby**.
232
+
233
+ It is **off by default** and turns on by env — a single-box ephemeral deploy pays
234
+ nothing; a durable deploy enables it by pointing at a bucket. The trigger is one
235
+ variable, `LITESTREAM_REPLICA_URL`:
236
+
237
+ - **empty (default):** the entrypoint `exec`s Falcon directly. The Litestream binary
238
+ is never invoked — behavior identical to not having it.
239
+ - **set:** on a fresh box the entrypoint **restores** `insika.db` from the replica
240
+ *before* the app opens it (`litestream restore -if-replica-exists`; a no-op if the
241
+ bucket is still empty), then **supervises** the app (`litestream replicate -exec`),
242
+ replicating the WAL continuously and doing a final sync on shutdown (Railway's
243
+ SIGTERM).
244
+
245
+ ### Enable in production (Railway)
246
+
247
+ Add the vars (keep the volume at `/data`):
248
+
249
+ ```bash
250
+ # AWS S3
251
+ LITESTREAM_REPLICA_URL=s3://my-bucket/insika
252
+ LITESTREAM_REGION=us-east-1
253
+ LITESTREAM_ACCESS_KEY_ID=AKIA...
254
+ LITESTREAM_SECRET_ACCESS_KEY=...
255
+
256
+ # Cloudflare R2 (S3-compatible): same, + endpoint and region=auto
257
+ LITESTREAM_REPLICA_URL=s3://my-bucket/insika
258
+ LITESTREAM_ENDPOINT=https://<accountid>.r2.cloudflarestorage.com
259
+ LITESTREAM_REGION=auto
260
+ LITESTREAM_ACCESS_KEY_ID=...
261
+ LITESTREAM_SECRET_ACCESS_KEY=...
262
+ ```
263
+
264
+ Credentials are read natively by Litestream (they are not in `deploy/litestream.yml`,
265
+ which only references URL/endpoint/region via `${VAR}`).
266
+
267
+ ### Restore drill (the real "done" — an untested backup does not count)
268
+
269
+ The pilot→production gap only closes once a restore has been **exercised**. Two ways:
270
+
271
+ **1. Local, automated (proves the mechanism, zero credentials):** uses the real
272
+ image + a `file://` replica; boots → replicates → deletes the volume → boots a new
273
+ box → restores → confirms the marker row survived and `/up` is green.
274
+
275
+ ```bash
276
+ scripts/litestream-restore-drill.sh # needs docker, sqlite3, curl
277
+ # → [drill] PASS — marker … restored from replica; /up green on the new box.
278
+ ```
279
+
280
+ **2. Production (the drill that counts for go-live):** against the real bucket.
281
+
282
+ ```bash
283
+ # a. with the service live and replicating, generate some config/conversation and
284
+ # confirm the replica has generations:
285
+ litestream snapshots -config deploy/litestream.yml "$INSIKA_DB"
286
+
287
+ # b. boot a NEW box (empty volume) with the same LITESTREAM_* vars → the entrypoint
288
+ # restores on boot. Verify manually in /studio that conversations and config came
289
+ # back. Manual restore alternative:
290
+ litestream restore -config deploy/litestream.yml -o /tmp/restored.db "$INSIKA_DB"
291
+ ```
292
+
293
+ ## Kubernetes (evolution)
294
+
295
+ SQLite does not share one file across nodes. Paths forward: a StatefulSet + a PVC
296
+ per pod + **sticky-by-agent** routing (shard by tenant), or **LiteFS**, or an
297
+ optional **Postgres** adapter. **Litestream** (above) for backup/DR from day one —
298
+ orthogonal to topology.
299
+
300
+ ---
301
+
302
+ ## Measuring performance / load
303
+
304
+ ### 1. SQLite write ceiling (no provider) — `bench_store.rb`
305
+
306
+ Isolates "can SQLite take multi-process writes?" from LLM noise: N processes
307
+ hammering writes on the **same** file (WAL + busy_timeout — the real config).
308
+
309
+ ```bash
310
+ bundle exec ruby scripts/bench_store.rb 1,2,4,8 3000
311
+ ```
312
+
313
+ **Measured (mid-2026, laptop, ~481B payload):**
314
+
315
+ | procs | writes/s (aggregate) | p50 | p95 | max | locked |
316
+ |------:|--------------------:|----:|----:|----:|-------:|
317
+ | 1 | ~29.6k | 0.02ms | 0.04ms | 2.4ms | **0** |
318
+ | 2 | ~29.9k | 0.02ms | 0.04ms | 59ms | **0** |
319
+ | 4 | ~23.7k | 0.02ms | 0.05ms | 336ms | **0** |
320
+ | 8 | ~28.4k | 0.03ms | 0.05ms | 539ms | **0** |
321
+
322
+ **Reading:** aggregate throughput stays ~25–30k writes/s regardless of process
323
+ count (the WAL's one-writer-at-a-time ceiling), with **zero "database is locked"**
324
+ (the `busy_timeout` absorbs contention into tail latency, not errors), and a
325
+ microscopic p95. A real turn is **provider-bound (seconds)** and does a handful of
326
+ writes → the workload sits ~100× under the ceiling. **Empirically, SQLite is not
327
+ the bottleneck on a single box.**
328
+
329
+ ### 2. End-to-end load (with provider) — `loadtest.rb`
330
+
331
+ Hits `POST /v1/responses` (SSE), the production path. Measures TTFB, total, tokens,
332
+ cache hits, P50/P95, error rate. Runs against local or a remote deployment. See
333
+ [LOADTEST.md](LOADTEST.md).
334
+
335
+ ```bash
336
+ INSIKA_URL=http://localhost:9292 OPENCLAW_GATEWAY_TOKEN=xxx \
337
+ bundle exec ruby scripts/loadtest.rb --agents assistant --concurrency 16 --iterations 3
338
+ ```
339
+
340
+ ### 3. Baseline vs multi-worker on one box — `loadtest-local.sh`
341
+
342
+ Boots Falcon with 1 worker, then N, over the **same** SQLite, and counts "database
343
+ is locked" in the logs.
344
+
345
+ ```bash
346
+ DEEPSEEK_API_KEY=sk-... ./scripts/loadtest-local.sh 4 24
347
+ ```
348
+
349
+ ## See also
350
+
351
+ - [RUNNING-LOCAL.md](RUNNING-LOCAL.md) — run the engine locally, single-process.
352
+ - [Security](SECURITY.md) — tokens, egress, strict config.
353
+ - [BENCHMARK.md](BENCHMARK.md) — the neutral, key-free engine benchmark.
354
+ - [OBSERVABILITY.md](OBSERVABILITY.md) — OpenTelemetry traces + metrics (opt-in).
data/docs/EMBEDDING.md ADDED
@@ -0,0 +1,198 @@
1
+ ---
2
+ title: Embedding
3
+ parent: Ship it
4
+ nav_order: 4
5
+ permalink: /embedding/
6
+ ---
7
+
8
+ # Embedding
9
+
10
+ Insika does not have to be a service you stand up next to your app. It can be an
11
+ object inside the app you already have: a graph you build in an initializer and a
12
+ Rack app you mount in your router. This page is the contract for doing that —
13
+ what an embedded graph owns, what it still shares with the process, and what that
14
+ makes your responsibility.
15
+
16
+ ---
17
+
18
+ ## The short version
19
+
20
+ ```ruby
21
+ # config/initializers/insika.rb
22
+ INSIKA = Insika.embed(backend: Insika::Stores::SQLite.new(path: Rails.root.join("storage/insika.sqlite3").to_s)) do
23
+ agent "support" do
24
+ model "deepseek-v4-flash"
25
+ provider :deepseek
26
+ api_key ENV.fetch("DEEPSEEK_API_KEY")
27
+ instructions "You answer questions about orders. Be brief."
28
+ end
29
+ end
30
+
31
+ # turns are born as children of a long-lived supervisor, not of the request
32
+ INSIKA.runtime.graph.executor.supervised = true
33
+ ```
34
+
35
+ ```ruby
36
+ # config/routes.rb — the transport is lazy: `require "insika"` never loads it
37
+ require "insika/server/rack_app"
38
+
39
+ mount Insika::Server.rack_app(INSIKA, token: ENV.fetch("INSIKA_TOKEN")), at: "/ai"
40
+ ```
41
+
42
+ ```ruby
43
+ # anywhere in your app — no HTTP involved
44
+ INSIKA.reply("support", "where is order 8123?", session: current_user.id)
45
+ ```
46
+
47
+ `Insika.embed` takes the same block as [`Insika.system`](AGENTS.md) and adds one
48
+ obligation: **you name the store**. That single argument is what turns "one
49
+ program" into "one object" — see [why](#why-the-store-is-an-argument) below.
50
+
51
+ `Insika::Server.rack_app` returns a plain Rack app. It routes on `PATH_INFO`, so
52
+ Rails' `mount` (and `Rack::URLMap`, and anything else that moves the prefix into
53
+ `SCRIPT_NAME`) leaves every route intact: mounted at `/ai`, the drop-in API is at
54
+ `/ai/v1/responses`.
55
+
56
+ ---
57
+
58
+ ## Why the store is an argument
59
+
60
+ The engine was not always one process — it was one *program*. Two graphs
61
+ built in the same Ruby process shared things they never declared:
62
+
63
+ | What | What actually happened |
64
+ |---|---|
65
+ | LLM credentials | one process-wide slot **per provider**: two graphs on `deepseek` with different keys, and the second one won **for both**. No error, no warning — agent A simply called the provider with tenant B's key. |
66
+ | The store | `INSIKA_DB` unset → each graph got its own in-memory store and nothing persisted; `INSIKA_DB` set → every graph opened the **same file** and read the others' agents, sessions and tasks. |
67
+
68
+ Neither failure raised. That is the whole reason the embed contract is written
69
+ down rather than left to good sense.
70
+
71
+ ---
72
+
73
+ ## The contract
74
+
75
+ ### 1. A graph owns its store
76
+
77
+ Every durable thing — sessions, tasks, checkpoints, the outbox, delegations,
78
+ memory — belongs to the backend the graph was built with. Two graphs given the
79
+ **same** backend share all of it, by your choice. Two graphs given **different**
80
+ backends share nothing.
81
+
82
+ ```ruby
83
+ support = Insika.embed(backend: Insika::Stores::SQLite.new(path: "storage/support.sqlite3")) { … }
84
+ ops = Insika.embed(backend: Insika::Stores::SQLite.new(path: "storage/ops.sqlite3")) { … }
85
+ # a session created in `support` does not exist in `ops`
86
+ ```
87
+
88
+ ### 2. A graph owns its LLM credentials
89
+
90
+ Provider keys and bases resolve through the graph's own
91
+ [`RubyLLM.context`](https://rubyllm.com) — an isolated dup of the configuration.
92
+ An embedded graph never mutates the global `RubyLLM.config`, and never reads
93
+ another graph's credentials. That holds for the turn itself *and* for everything
94
+ else in the graph that asks a model: the content-safety moderator and the output
95
+ validator ask on the same context the turn does.
96
+
97
+ It also holds for runtime reconfiguration. Editing a provider key in the Studio
98
+ (or dispatching `:upsert_llm_provider`) applies to **that graph only** — the
99
+ change is real, takes effect without a restart, and stops at the graph boundary.
100
+
101
+ ### 3. The process still owns signals and the reactor
102
+
103
+ Draining in-flight turns on SIGTERM ([the process model](DEPLOY.md)) is a process
104
+ concern, not a graph concern: `Signal.trap` keeps only the last handler, so a
105
+ shutdown installed once per graph would drain the last graph and let the others
106
+ die mid-turn. An embedded graph therefore installs **nothing** behind your back.
107
+ You install it once, naming every graph:
108
+
109
+ ```ruby
110
+ Insika::Shutdown.install(executors: [SUPPORT, OPS].map { |g| g.runtime.graph.executor })
111
+ ```
112
+
113
+ One signal closes every graph's intake first, then waits — up to
114
+ `INSIKA_DRAIN_TIMEOUT` (default 20s) — for the in-flight turns of all of them.
115
+ Whatever outlives the deadline stays `:running` and is replayed by the next
116
+ boot's recovery sweep.
117
+
118
+ The reactor is yours too, and this one has teeth:
119
+
120
+ > **The mounted app needs an async server.** `rack_app` is a value — nothing in it
121
+ > starts a server or a reactor. A turn is a fiber, so the routes that start one
122
+ > (`POST /v1/responses`, `/v1/messages`, `/v1/commands`, `POST /channels/:id/events`)
123
+ > must be called from inside a running reactor. Under **Falcon** you already are.
124
+ > Under Puma or Unicorn they answer `500 {"error":{"message":"No async task
125
+ > available!"}}` — the read-only routes (`GET /v1/agents/:id`, `/v1/tasks/:id`, `/up`)
126
+ > are fine either way. Serve an embedded Insika from Falcon, or reach the graph
127
+ > in-process with `INSIKA.reply(...)`, which builds its own reactor.
128
+
129
+ And once you are inside one, set `supervised = true` (as in the quickstart): it
130
+ makes a turn a child of a long-lived supervisor instead of a child of the
131
+ request, so the turn survives a client disconnect. Without it the runtime cancels
132
+ the turn when the connection drops.
133
+
134
+ ### 4. The Studio is not part of the embeddable surface
135
+
136
+ `rack_app` returns the `/v1` transport and nothing else. `Studio::App.configure`
137
+ freezes its collaborators on the **class**, which makes it one operator UI per
138
+ process: a second graph configuring the Studio replaces the first's wiring, for
139
+ both. So a host that wants the UI does a plain `require "insika/studio/app"`,
140
+ calls `Studio::App.configure` with exactly one graph's collaborators, and mounts
141
+ the class — accepting that the UI shows that graph and no other.
142
+
143
+ This is a stated limitation, not an oversight. Making the Studio instantiable is
144
+ its own change, and it needs its own reason.
145
+
146
+ ### 5. ENV is a default, never a requirement
147
+
148
+ Anything an embedded graph reads from the environment has an injectable
149
+ equivalent, and the injected value wins. `INSIKA_DB` still works — it is the
150
+ default for the DSL quickstart and for `config.ru` — but an embedded graph that
151
+ was given a backend never looks at it.
152
+
153
+ ---
154
+
155
+ ## What you are responsible for
156
+
157
+ | Concern | Who |
158
+ |---|---|
159
+ | Which store each graph gets | You, via `backend:` |
160
+ | Which credentials each graph gets | You, via the agent's `api_key`/`provider` |
161
+ | Who may call the mounted app | You. `token:` is one shared Bearer for the whole mount — see below |
162
+ | Signals and the drain | You, once per process (`Shutdown.install(executors:)`) |
163
+ | The reactor / `supervised` | You, matching your server |
164
+ | Recovery of orphaned turns at boot | You, if you want it — the sweep is `Insika::Recovery`, wired by `Insika::Server::Boot` for the standalone deployment, not by `embed` |
165
+
166
+ ### Embedding is not the multi-tenant server
167
+
168
+ Two graphs stop corrupting each other. That is all this contract says. **Who is
169
+ allowed to talk to which graph** is authorization, and it is not here: `token:` is
170
+ a single Bearer gating the whole mounted app, exactly as it does for the
171
+ standalone server in the default mode. Multi-tenancy is a property of the
172
+ *standalone* server, not of an embed: with `INSIKA_TENANCY=multi_tenant` the
173
+ Bearer resolves to a principal before the routes — per-tenant tokens scoped to
174
+ their own sessions, tasks and events, operator tokens with the run of the
175
+ deployment (see [Security](SECURITY.md#the-bearer-gate)). An embedded graph keeps
176
+ no token store, so an embed's tenancy belongs to the host: put the mounted app
177
+ behind your own authentication and pass `session:` yourself — do not hand the
178
+ mount point to the browser.
179
+
180
+ ---
181
+
182
+ ## What this is not
183
+
184
+ - **Not a Rails engine.** `insika-rails` would be a separate gem; this is the Rack
185
+ app such a gem would mount. The core has no Rails knowledge and takes no Rails
186
+ dependency.
187
+ - **Not a second assembly path.** `Insika.embed` is a front door over the same
188
+ pipeline `Insika.agent`/`Insika.system` use — the profile it produces is
189
+ identical, and there is a spec that holds it to that.
190
+
191
+ ---
192
+
193
+ ## See also
194
+
195
+ - [Architecture](ARCHITECTURE.md) — how a turn actually runs.
196
+ - [Deploy](DEPLOY.md) — the standalone process model (N workers of *one*
197
+ deployment), which is a different question from N graphs in one process.
198
+ - [Agents](AGENTS.md) — the DSL block `embed` takes.