insika 0.3.0 → 0.8.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 (204) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +296 -0
  3. data/README.md +48 -12
  4. data/bin/insika +725 -0
  5. data/bin/insika-router +87 -0
  6. data/docs/AGENTS.md +116 -406
  7. data/docs/API.md +5 -5
  8. data/docs/ARCHITECTURE.md +3 -2
  9. data/docs/ARTIFACTS.md +137 -0
  10. data/docs/BENCHMARK.md +2 -2
  11. data/docs/CHANNELS.md +14 -14
  12. data/docs/CONTEXT.md +63 -19
  13. data/docs/DEMO.md +80 -0
  14. data/docs/DEPLOY.md +87 -10
  15. data/docs/EMBEDDING.md +1 -1
  16. data/docs/EVALS.md +128 -3
  17. data/docs/FACTS.md +3 -3
  18. data/docs/HARVEST.md +5 -6
  19. data/docs/KNOWLEDGE.md +290 -0
  20. data/docs/LOADTEST.md +17 -29
  21. data/docs/MEDIA.md +128 -0
  22. data/docs/OBSERVABILITY.md +46 -12
  23. data/docs/OUTCOMES.md +137 -0
  24. data/docs/PLUGINS.md +51 -6
  25. data/docs/POLICY.md +222 -0
  26. data/docs/REFINEMENT.md +14 -9
  27. data/docs/RELEASING.md +4 -4
  28. data/docs/ROUTER.md +213 -0
  29. data/docs/RUNNING-LOCAL.md +5 -5
  30. data/docs/SCHEDULING.md +121 -0
  31. data/docs/SECURITY.md +23 -7
  32. data/docs/SKILLS.md +11 -2
  33. data/docs/SOAK.md +3 -3
  34. data/docs/TEMPLATES.md +134 -0
  35. data/docs/TOOLS.md +176 -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 +4 -4
  47. data/docs/improve.md +20 -0
  48. data/docs/index.md +8 -5
  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 +99 -17
  59. data/lib/insika/artifact_signing.rb +82 -0
  60. data/lib/insika/artifact_store.rb +160 -0
  61. data/lib/insika/channel_delivery.rb +1 -1
  62. data/lib/insika/chat_builder.rb +50 -19
  63. data/lib/insika/commands/agent_payload.rb +2 -2
  64. data/lib/insika/commands/backfill_knowledge.rb +145 -0
  65. data/lib/insika/commands/delete_artifact.rb +35 -0
  66. data/lib/insika/commands/delete_concept.rb +34 -0
  67. data/lib/insika/commands/delete_mcp.rb +6 -2
  68. data/lib/insika/commands/delete_tenant_data.rb +15 -3
  69. data/lib/insika/commands/gate_refinement.rb +1 -1
  70. data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
  71. data/lib/insika/commands/restore_concept.rb +34 -0
  72. data/lib/insika/commands/seed_demo_data.rb +31 -0
  73. data/lib/insika/commands/upsert_mcp.rb +6 -3
  74. data/lib/insika/commands/write_concept.rb +57 -0
  75. data/lib/insika/compaction.rb +196 -0
  76. data/lib/insika/context/builder.rb +6 -2
  77. data/lib/insika/context/fragment.rb +4 -1
  78. data/lib/insika/context/priority.rb +8 -0
  79. data/lib/insika/context/providers/briefing.rb +53 -24
  80. data/lib/insika/context/providers/knowledge.rb +108 -0
  81. data/lib/insika/context/providers/prompt.rb +30 -24
  82. data/lib/insika/context/providers/session.rb +46 -10
  83. data/lib/insika/context_trace_store.rb +11 -1
  84. data/lib/insika/cron.rb +189 -0
  85. data/lib/insika/demo/agent_attrs.rb +43 -0
  86. data/lib/insika/demo/golden_cases.rb +81 -0
  87. data/lib/insika/demo/seeder.rb +336 -0
  88. data/lib/insika/doctor.rb +280 -17
  89. data/lib/insika/dsl/definition.rb +3 -2
  90. data/lib/insika/dsl/runtime.rb +64 -79
  91. data/lib/insika/dsl/server_boot.rb +23 -1
  92. data/lib/insika/dsl/system.rb +10 -2
  93. data/lib/insika/dsl.rb +103 -2
  94. data/lib/insika/env_schema.rb +21 -7
  95. data/lib/insika/evals/golden.rb +41 -4
  96. data/lib/insika/evals/judge.rb +47 -2
  97. data/lib/insika/evals/pairwise.rb +11 -0
  98. data/lib/insika/evals/persona.rb +98 -0
  99. data/lib/insika/evals/runner.rb +9 -0
  100. data/lib/insika/evals/simulator.rb +225 -0
  101. data/lib/insika/evals/transport.rb +84 -2
  102. data/lib/insika/event_stream.rb +10 -0
  103. data/lib/insika/executor.rb +295 -55
  104. data/lib/insika/followup_policy.rb +2 -25
  105. data/lib/insika/golden_store.rb +16 -1
  106. data/lib/insika/grounding/matcher.rb +1 -1
  107. data/lib/insika/knowledge.rb +680 -0
  108. data/lib/insika/knowledge_store.rb +140 -0
  109. data/lib/insika/loop_detector.rb +5 -34
  110. data/lib/insika/mcp_client.rb +94 -0
  111. data/lib/insika/mcp_json.rb +74 -0
  112. data/lib/insika/mcp_live_tool.rb +43 -0
  113. data/lib/insika/mcp_store.rb +98 -26
  114. data/lib/insika/mcp_tool_ingestor.rb +30 -8
  115. data/lib/insika/mcp_tool_registry.rb +100 -0
  116. data/lib/insika/media.rb +115 -31
  117. data/lib/insika/message_origin.rb +1 -1
  118. data/lib/insika/middleware.rb +9 -0
  119. data/lib/insika/onboarding.rb +17 -1
  120. data/lib/insika/outcome_store.rb +1 -1
  121. data/lib/insika/overlay_tool_registry.rb +37 -17
  122. data/lib/insika/packaging.rb +2 -2
  123. data/lib/insika/profile_source.rb +15 -1
  124. data/lib/insika/prompt_catalog.rb +10 -0
  125. data/lib/insika/retention.rb +36 -1
  126. data/lib/insika/router/app.rb +157 -0
  127. data/lib/insika/router/backend_pool.rb +98 -0
  128. data/lib/insika/router/hash_ring.rb +55 -0
  129. data/lib/insika/router/proxy_body.rb +34 -0
  130. data/lib/insika/router/session_key.rb +54 -0
  131. data/lib/insika/router.rb +18 -0
  132. data/lib/insika/schedule.rb +177 -0
  133. data/lib/insika/schedule_engine.rb +314 -0
  134. data/lib/insika/schedule_store.rb +208 -0
  135. data/lib/insika/server/app.rb +105 -15
  136. data/lib/insika/server/rack_app.rb +5 -1
  137. data/lib/insika/server/responses.rb +5 -5
  138. data/lib/insika/session_store.rb +34 -4
  139. data/lib/insika/settings_store.rb +8 -1
  140. data/lib/insika/skill_catalog.rb +12 -0
  141. data/lib/insika/soak/runner.rb +4 -4
  142. data/lib/insika/steer_injector.rb +21 -10
  143. data/lib/insika/studio/app.rb +591 -47
  144. data/lib/insika/studio/assets/dist/application.css +1 -1
  145. data/lib/insika/studio/assets/dist/application.js +21 -21
  146. data/lib/insika/studio/forms.rb +57 -5
  147. data/lib/insika/studio/nav_icons.rb +14 -1
  148. data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
  149. data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
  150. data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
  151. data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
  152. data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
  153. data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
  154. data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
  155. data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
  156. data/lib/insika/studio/views/_agents_master.erb +44 -0
  157. data/lib/insika/studio/views/_message.erb +49 -32
  158. data/lib/insika/studio/views/agent_detail.erb +61 -820
  159. data/lib/insika/studio/views/agents.erb +70 -57
  160. data/lib/insika/studio/views/artifact.erb +23 -0
  161. data/lib/insika/studio/views/artifacts.erb +59 -0
  162. data/lib/insika/studio/views/evals.erb +2 -2
  163. data/lib/insika/studio/views/facts.erb +1 -1
  164. data/lib/insika/studio/views/funnel.erb +1 -1
  165. data/lib/insika/studio/views/home.erb +106 -67
  166. data/lib/insika/studio/views/knowledge.erb +123 -0
  167. data/lib/insika/studio/views/layout.erb +14 -11
  168. data/lib/insika/studio/views/mcp.erb +174 -80
  169. data/lib/insika/studio/views/session.erb +231 -177
  170. data/lib/insika/studio/views/settings.erb +50 -1
  171. data/lib/insika/studio/views/skills.erb +1 -1
  172. data/lib/insika/studio/views/tools.erb +24 -9
  173. data/lib/insika/telemetry/recorder.rb +49 -1
  174. data/lib/insika/templates/browser-agent/README.md +36 -0
  175. data/lib/insika/templates/browser-agent/agent.rb +49 -0
  176. data/lib/insika/templates/daily-digest/README.md +47 -0
  177. data/lib/insika/templates/daily-digest/agent.rb +77 -0
  178. data/lib/insika/templates/repo-explorer/README.md +36 -0
  179. data/lib/insika/templates/repo-explorer/agent.rb +45 -0
  180. data/lib/insika/templates/research-analyst/README.md +26 -0
  181. data/lib/insika/templates/research-analyst/agent.rb +68 -0
  182. data/lib/insika/templates/review-panel/README.md +20 -0
  183. data/lib/insika/templates/review-panel/agent.rb +50 -0
  184. data/lib/insika/templates/travel-planner/README.md +35 -0
  185. data/lib/insika/templates/travel-planner/agent.rb +87 -0
  186. data/lib/insika/templates.rb +112 -0
  187. data/lib/insika/tick.rb +24 -12
  188. data/lib/insika/timezone.rb +45 -0
  189. data/lib/insika/tool_batch.rb +67 -0
  190. data/lib/insika/tool_usage_report.rb +162 -0
  191. data/lib/insika/tools/generate_image.rb +52 -7
  192. data/lib/insika/tools/load_knowledge.rb +74 -0
  193. data/lib/insika/tools/run_persona_eval.rb +328 -0
  194. data/lib/insika/tools/save_artifact.rb +95 -0
  195. data/lib/insika/turn_budget.rb +91 -0
  196. data/lib/insika/turn_output.rb +1 -1
  197. data/lib/insika/turn_state.rb +15 -4
  198. data/lib/insika/version.rb +1 -1
  199. data/lib/insika/wiring/graph.rb +184 -12
  200. data/lib/insika/wiring/graph_chat.rb +102 -0
  201. data/lib/insika.rb +64 -0
  202. metadata +109 -5
  203. data/docs/build.md +0 -14
  204. data/docs/understand.md +0 -10
data/docs/ROUTER.md ADDED
@@ -0,0 +1,213 @@
1
+ ---
2
+ title: Router
3
+ parent: Ship it
4
+ nav_order: 4
5
+ permalink: /router/
6
+ ---
7
+
8
+ # Session-sticky router
9
+
10
+ `insika-router` is a standalone proxy that lets you run **N engine backends**
11
+ (`WEB_CONCURRENCY=1` each) and get the same per-session guarantees a single
12
+ worker gives you today — FIFO ordering, `collect`/`steer`, the SSE watch (see
13
+ [DEPLOY.md "The process model"](DEPLOY.md#the-process-model)) — at N>1
14
+ capacity. It is **entirely opt-in**: if one worker is enough for you, ignore
15
+ this file, run the engine exactly as DEPLOY.md already describes, and nothing
16
+ changes. Reach for the router only when you outgrow one worker and want to
17
+ scale up.
18
+
19
+ It changes nothing about the engine itself — no code in `SessionActor` or
20
+ `Executor` is aware the router exists. It solves routing, and routing only: a
21
+ given session's requests always land on the same backend, so that backend's
22
+ in-memory session state is always the one being read and written.
23
+
24
+ ## Why this exists
25
+
26
+ `WEB_CONCURRENCY>1` without sticky routing in front is not "reduced
27
+ guarantees" — it is a correctness bug (a reply from one session can leak into
28
+ another's transcript; `insika doctor`'s `web-concurrency` check exists because
29
+ this happened in staging). Sticky routing is the documented escape hatch, but
30
+ neither deploy target the engine ships for has it built in:
31
+
32
+ - **Railway** does not support sticky sessions at all — traffic is randomly
33
+ distributed across replicas, with no configuration that changes that.
34
+ - **Kubernetes** `Service` load-balances with no session notion, and
35
+ ingress-nginx's `upstream-hash-by` (the usual sticky mechanism) hashes on
36
+ nginx *variables* — headers, cookies, the URL — never a field parsed out of
37
+ a POST body. The session id here is exactly that: the `user` field inside
38
+ `POST /v1/responses`'s JSON body.
39
+
40
+ So this is a small piece of new infrastructure, not a config flag.
41
+
42
+ ## How it decides where a request goes
43
+
44
+ Per request, in order:
45
+
46
+ 1. `GET /up` → answered directly by the router (its own liveness), never
47
+ proxied.
48
+ 2. `POST /v1/responses` or `POST /v1/messages` → the session key is the
49
+ `user` field of the JSON body.
50
+ 3. `POST /channels/:id/messages` or `POST /channels/:id/events` (the web
51
+ widget and the relay channel) → the session key is the `session_id` field
52
+ of the JSON body.
53
+ 4. Everything else (health checks, `/studio/*`, onboarding, minting a new
54
+ channel session) → no session key, plain round-robin. None of these depend
55
+ on a worker's in-memory `SessionActor` — a Studio read hits the durable
56
+ store, and minting a session has no existing state to be sticky about.
57
+
58
+ A request with a session key is routed by a ketama-style **consistent hash
59
+ ring** over the backend list: the same key always reaches the same backend,
60
+ and adding or removing one backend remaps only ~1/N of the key space, not the
61
+ whole ring — a rolling deploy does not bounce every live session to a new
62
+ owner at once. A request whose key isn't found (a corner-case route) or whose
63
+ key extraction is skipped (see body size cap below) round-robins across all
64
+ backends.
65
+
66
+ The whole request body is always read and forwarded byte-for-byte —
67
+ `INSIKA_ROUTER_BODY_MAX_BYTES` (default 256 KiB) only bounds how much of it
68
+ the router will attempt to parse as JSON while looking for a session key; a
69
+ body over that cap round-robins instead of erroring, and the router logs it.
70
+
71
+ **A request whose chosen backend is unreachable is never retried against a
72
+ different backend** — that backend may already hold a durable, at-most-once
73
+ claim on the task the request names, and retrying elsewhere could
74
+ double-process it. It answers the same retry envelope a single overloaded
75
+ backend would:
76
+
77
+ ```json
78
+ {"error": {"class": "Insika::Router::BackendUnavailable", "message": "no backend reachable",
79
+ "retryable": true, "retry_after": 1}}
80
+ ```
81
+
82
+ SSE responses stream through the router with no added buffering — a client
83
+ watching a long turn sees the same chunks, in the same order, as if it had
84
+ hit the backend directly.
85
+
86
+ ## Deploy shape 1 — Railway (N local workers, one replica)
87
+
88
+ Railway's replica load balancer has no sticky option, full stop — this shape
89
+ does not attempt to fix that. What it fixes is the *unsafe* alternative
90
+ (`WEB_CONCURRENCY=N` Falcon workers behind Railway's own port, which is
91
+ exactly the leak `insika doctor` errors on). Instead, run N engine processes
92
+ on different local ports and put the router in front of them, all inside the
93
+ one container Railway load-balances to:
94
+
95
+ ```bash
96
+ # three engine workers, WEB_CONCURRENCY=1 each (the entrypoint's own
97
+ # `falcon serve --count 1`), on different local ports — never `--count 3` on
98
+ # one port, which is exactly the unsafe fan-out this replaces
99
+ bundle exec falcon serve --bind http://127.0.0.1:9292 --count 1 config.ru &
100
+ bundle exec falcon serve --bind http://127.0.0.1:9293 --count 1 config.ru &
101
+ bundle exec falcon serve --bind http://127.0.0.1:9294 --count 1 config.ru &
102
+
103
+ # the router, bound to the port Railway actually forwards
104
+ INSIKA_ROUTER_PORT=$PORT \
105
+ INSIKA_ROUTER_BACKENDS=http://127.0.0.1:9292,http://127.0.0.1:9293,http://127.0.0.1:9294 \
106
+ bundle exec insika-router
107
+ ```
108
+
109
+ `insika doctor` treats `WEB_CONCURRENCY>1` as `ok` (not `error`/`warn`) once
110
+ it sees `INSIKA_ROUTER_BACKENDS` or `INSIKA_ROUTER_BACKENDS_DNS` set — it
111
+ cannot verify a router process is actually running at those addresses, only
112
+ that one was configured, same as every other env-based capability check in
113
+ `insika doctor`.
114
+
115
+ ## Deploy shape 2 — Kubernetes (a headless Service)
116
+
117
+ Run N engine pods (`WEB_CONCURRENCY=1` each) behind a **headless** Service
118
+ (`clusterIP: None` — this is what makes DNS resolve to one A/AAAA record per
119
+ ready pod instead of a single virtual IP), and the router as its own
120
+ Deployment in front:
121
+
122
+ ```yaml
123
+ apiVersion: v1
124
+ kind: Service
125
+ metadata:
126
+ name: insika-headless
127
+ spec:
128
+ clusterIP: None
129
+ selector: { app: insika }
130
+ ports: [{ port: 9292 }]
131
+ ---
132
+ # insika-router Deployment env:
133
+ env:
134
+ - name: INSIKA_ROUTER_BACKENDS_DNS
135
+ value: insika-headless.default.svc.cluster.local
136
+ - name: INSIKA_ROUTER_BACKEND_PORT
137
+ value: "9292"
138
+ - name: INSIKA_ROUTER_DNS_INTERVAL
139
+ value: "15"
140
+ ```
141
+
142
+ The router holds no session state itself, so it needs no sticky routing in
143
+ front of *itself* — it scales trivially (1-2 replicas behind an ordinary
144
+ `Service`). It re-resolves the headless Service on `INSIKA_ROUTER_DNS_INTERVAL`
145
+ (default 15s) and rebuilds its hash ring only when the resolved pod set
146
+ actually changed. A pod that just became ready is invisible to the router
147
+ until the next resolve — capacity added a few seconds late, never wrong.
148
+
149
+ ## Environment variables
150
+
151
+ | Variable | Default | Meaning |
152
+ |---|---|---|
153
+ | `INSIKA_ROUTER_BACKENDS` | — | Comma-separated backend URLs (static mode). Exactly one of this or the DNS var below. |
154
+ | `INSIKA_ROUTER_BACKENDS_DNS` | — | A headless-Service hostname to re-resolve (DNS mode). Requires `INSIKA_ROUTER_BACKEND_PORT`. |
155
+ | `INSIKA_ROUTER_BACKEND_PORT` | — | The engine port on every DNS-resolved pod. |
156
+ | `INSIKA_ROUTER_DNS_INTERVAL` | `15` | Seconds between DNS re-resolves. |
157
+ | `INSIKA_ROUTER_BODY_MAX_BYTES` | `262144` | Size cap on the session-key JSON peek (never on what is forwarded). |
158
+ | `INSIKA_ROUTER_BACKEND_TIMEOUT` | `10` | Connect/read timeout to a backend, in seconds. |
159
+ | `INSIKA_ROUTER_HOST` | `0.0.0.0` | Bind address for the router itself. |
160
+ | `INSIKA_ROUTER_PORT` | `9090` | Listen port for the router itself. |
161
+
162
+ ## A runnable smoke test
163
+
164
+ The shape below is what the router's acceptance criteria were verified
165
+ against: two fake backends and the router in front, run entirely in-process.
166
+
167
+ ```ruby
168
+ require "async"; require "async/http/server"; require "async/http/client"
169
+ require "async/http/endpoint"; require "protocol/rack"; require "insika/router"
170
+
171
+ Async do |task|
172
+ echo = ->(name) { ->(env) { [200, {}, ["#{name} #{Rack::Request.new(env).path_info}"]] } }
173
+ %w[9292 9293].each_with_index do |port, i|
174
+ endpoint = Async::HTTP::Endpoint.parse("http://127.0.0.1:#{port}")
175
+ task.async { Async::HTTP::Server.new(Protocol::Rack::Adapter.new(echo["backend-#{i}"]), endpoint).run }
176
+ end
177
+ task.sleep(0.2)
178
+
179
+ pool = Insika::Router::BackendPool.new(static: %w[http://127.0.0.1:9292 http://127.0.0.1:9293])
180
+ app = Insika::Router::App.new(pool: pool)
181
+ endpoint = Async::HTTP::Endpoint.parse("http://127.0.0.1:9090")
182
+ task.async { Async::HTTP::Server.new(Protocol::Rack::Adapter.new(app), endpoint).run }
183
+ task.sleep(0.2)
184
+
185
+ client = Async::HTTP::Client.new(Async::HTTP::Endpoint.parse("http://127.0.0.1:9090"))
186
+ 3.times { |i| puts client.post("/v1/responses", {}, [%({"user":"sess-1","i":#{i}})]).read }
187
+ # -> the same "backend-N" answers all three times, even though two backends are up.
188
+ ensure
189
+ task.stop
190
+ end
191
+ ```
192
+
193
+ ## What this deliberately does not attempt
194
+
195
+ - **Railway cross-*replica* routing.** Railway's replica load balancer itself
196
+ has no sticky option and this router cannot sit in front of Railway's own
197
+ edge. Railway stays at one replica; this only raises the ceiling of that one
198
+ replica (N local workers instead of N=1).
199
+ - **A distributed `SessionActor`.** The alternative design — making any
200
+ worker able to safely pick up any session, removing the need for sticky
201
+ routing entirely — is a much larger rewrite (debounce windows, steer
202
+ mailboxes, and SSE fan-out would all have to move into the shared store with
203
+ lease semantics) for the same outcome this router reaches with an unchanged
204
+ engine. Worth revisiting only if this approach turns out not to scale far
205
+ enough.
206
+ - Native WhatsApp/Slack channel routing — those channels are shelved; the
207
+ relay channel rides the same `/v1/responses`-shaped call this router already
208
+ covers.
209
+
210
+ ## See also
211
+
212
+ - [DEPLOY.md "The process model"](DEPLOY.md#the-process-model) — the contract
213
+ this router satisfies (FIFO/`collect`/`steer` guarantees, recovery, drain).
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Running locally
3
- parent: Build an agent
4
- nav_order: 8
3
+ parent: Start here
4
+ nav_order: 2
5
5
  permalink: /running-local/
6
6
  ---
7
7
 
@@ -62,7 +62,7 @@ whole surface answers `503`, never open by omission.
62
62
  | `INSIKA_DB` | — (ephemeral memory) | SQLite path → config + execution survive a restart |
63
63
  | `BIND` | `http://localhost:9292` | host:port |
64
64
  | `ADMIN_TOKEN` | `local-demo` | token for `/studio` |
65
- | `OPENCLAW_GATEWAY_TOKEN` | falls back to `ADMIN_TOKEN` | Bearer for the whole `/v1` + `/a2a` surface |
65
+ | `INSIKA_GATEWAY_TOKEN` | falls back to `ADMIN_TOKEN` | Bearer for the whole `/v1` + `/a2a` surface |
66
66
  | `DEEPSEEK_MODEL` | `deepseek-v4-flash` | model |
67
67
 
68
68
  With persistence:
@@ -122,13 +122,13 @@ prompt files, skills, and one data-tool per file:
122
122
  > **Data-tool URLs must be literal on the pack path.** The pack import does not
123
123
  > resolve `{{env.*}}` — bake the backend base URL into each `tools/*.json` at
124
124
  > generation time. (Only the *manifest* path resolves `{{env.*}}`.) See
125
- > [Tools](TOOLS.md#the-one-gotcha-env-templating-is-manifest-only).
125
+ > [Tools](TOOLS.md#the-one-gotcha-envsecret-templating-is-manifest-only).
126
126
 
127
127
  Provision it (runs as a client against the live server; the internal token comes
128
128
  from the environment, never disk):
129
129
 
130
130
  ```bash
131
- INSIKA_URL=http://localhost:9292 OPENCLAW_GATEWAY_TOKEN=local-demo \
131
+ INSIKA_URL=http://localhost:9292 INSIKA_GATEWAY_TOKEN=local-demo \
132
132
  bundle exec ruby scripts/import_pack.rb /path/to/pack
133
133
  ```
134
134
 
@@ -0,0 +1,121 @@
1
+ ---
2
+ title: Schedules
3
+ parent: Operate
4
+ nav_order: 2
5
+ permalink: /schedules/
6
+ ---
7
+
8
+ # Schedules — recurring turns the engine fires
9
+
10
+ A schedule is a turn nobody has to remember to send: a daily report at 22:00,
11
+ an eval sweep every night, a heartbeat every hour. The engine fires it on its
12
+ own periodic tick — no cron on some other box pointing at an authenticated
13
+ route. (That route still works, if you want it; the built-in trigger just
14
+ removes the homework.)
15
+
16
+ A schedule is **declared on the agent** — pack data, like `followup:` or the
17
+ budget — in one of the same three places every profile field is edited: the
18
+ DSL at import, `POST /v1/agents` in the pack, or the Studio's config form
19
+ (the **Schedules** group on the agent page). Edits are hot: the next pass
20
+ sees them.
21
+
22
+ ## The declaration
23
+
24
+ ```ruby
25
+ agent = Insika.agent("reporter") do
26
+ schedule "daily_report", cron: "0 22 * * *", tz: "America/Sao_Paulo",
27
+ message: "Run the daily report now.",
28
+ overrides: { turn_timeout: 900, max_tool_calls: 200 }
29
+ schedule "heartbeat", every: 3600, message: "Say you are alive."
30
+ end
31
+ ```
32
+
33
+ | Key | Meaning |
34
+ |---|---|
35
+ | `id` | the schedule's name (the argument). Lowercase, `[a-z][a-z0-9_-]*` |
36
+ | `cron` | a five-field expression (`minute hour day-of-month month day-of-week`), or |
37
+ | `every` | a plain interval in seconds — the two are **exclusive** |
38
+ | `tz` | IANA zone for **cron** materialization (default `Etc/UTC`). `every` never needs it — every comparison runs in UTC |
39
+ | `message` | the synthetic inbound that kicks each run — what the agent "hears" |
40
+ | `session_mode` | `"new"` (default) — a fresh session per run, the report shape; `"fixed"` — one standing session, the "standing assistant" shape |
41
+ | `session_id` | for `fixed` sessions: the standing session (created on first run when missing) |
42
+ | `overrides` | per-run ceilings: `turn_timeout`, `max_tool_calls`, `model` — a report needs a bigger ceiling than a chat turn; the base profile is untouched |
43
+ | `enabled` | `false` pauses the schedule (the Studio toggle / the JSON field) |
44
+
45
+ > **The cadence floor.** Firing rides one claim window per pass — a schedule
46
+ > fires **at most once per window**. That is the true cadence ceiling: an
47
+ > `every: 60` does not fire sixty times a minute, it fires once per window.
48
+ > `doctor` warns when a declared `every` is shorter than the claim window.
49
+
50
+ The Studio renders the schedules section as a JSON array of the same
51
+ declarations plus a read-only card: each schedule's next fire, last run and —
52
+ when a window was skipped — the skip reason. `doctor` parses every
53
+ declaration with the engine's own parser: an invalid cron, an unknown
54
+ runtime zone, a schedule with neither trigger, an unknown override key —
55
+ each is named, per agent, as an error finding.
56
+
57
+ ## The engine's triggers: cron subset
58
+
59
+ Five fields, whitespace-separated. Per field: `*` (or `?`), a single value, a
60
+ range (`N-M`), a step (`*/N`, `N-M/N`, `N/N`), or a comma list of those.
61
+ Day-of-week is `0-7` with `7` = Sunday; when **both** day fields are
62
+ restricted the date matches on **either** (standard cron OR semantics).
63
+ `L`, `W`, `#` and month/day names are refused loudly at creation — the engine
64
+ will not silently ignore a cron that only some dates understand.
65
+
66
+ ```text
67
+ minute hour day-of-month month day-of-week
68
+ 0 22 * * *
69
+ ```
70
+
71
+ ## Firing: one turn per window, no catch-up
72
+
73
+ Firing rides the tick, gated by its own claim window (one scheduler per
74
+ window across `N` workers — the same claim the outbox and recovery sweeps
75
+ use). Each due schedule is claimed transactionally: the task and the
76
+ schedule's state (last run, last task id, next fire) commit together, so two
77
+ workers racing serialize on the backend's lock and **exactly one fires per
78
+ window**.
79
+
80
+ Three skip rules are part of the contract, all recorded on the schedule and
81
+ shown in the Studio — never silent, never queued:
82
+
83
+ - **late** — the no-catch-up policy. A window more than one claim window in
84
+ the past is **missed, not replayed**: a deploy that was down over 22:00 does
85
+ not fire a 22:00 report at 06:00 the next morning. The schedule's lattice
86
+ advances to the next window and the skip is recorded.
87
+ - **overlap** — the previous run's task is still live (`queued`/`running`/
88
+ `waiting`/`paused`). The window is skipped, recorded, and the next one
89
+ fires. There is no queue buildup: one scheduled run at a time per schedule.
90
+ - **budget** — a **hard** calendar budget (`budget daily: …` on the profile)
91
+ already at/over its cap. The edge would fail the turn anyway; the engine
92
+ refuses to even queue it. A `soft` budget crosses and runs — the ledger
93
+ warns as usual.
94
+
95
+ A bounded run also costs what it costs: the run's usage lands on the same
96
+ `BudgetLedger` the edge enforces, and `billed = total + cached +
97
+ cache_creation` — a long report is cache-heavy, measure the caps against it.
98
+
99
+ ## What a schedule run is
100
+
101
+ A first-class turn, stamped `origin: "scheduled"` so a refinement read can
102
+ never mistake the engine's kick for a customer speaking. It enters through the
103
+ pipeline directly (never through the message edge, so no rate-limit/token
104
+ ceiling gate applies — same as a resume), is charged to the ledger like any
105
+ turn, and its result is delivered wherever the agent's outputs go: a channel
106
+ answer, or – for the report shape – nothing at all, when the run publishes an
107
+ artifact instead. The Studio's schedule card links the last run's task.
108
+
109
+ No customer ever receives anything from a schedule unless your agent sends a
110
+ message in reply — the engine contacts no one.
111
+
112
+ ## The boundaries
113
+
114
+ - **Not a job queue.** No priorities, no fan-out, no retries of a failed run
115
+ beyond what Recovery already does for any task.
116
+ - **Not the follow-up feature.** `schedule_followup` is a one-shot,
117
+ customer-facing, consent-gated contact from inside a conversation — and it
118
+ keeps being that. These schedules are operator-declared recurring internal
119
+ triggers: no contact policy, no consent, no customer.
120
+ - **Not a replacement for your cron.** The external route stays; this is the
121
+ built-in one.
data/docs/SECURITY.md CHANGED
@@ -27,7 +27,7 @@ The layers, from the edge inward:
27
27
  ## The Bearer gate
28
28
 
29
29
  The `/v1` and `/a2a` surface answers only with
30
- `Authorization: Bearer <OPENCLAW_GATEWAY_TOKEN>` (which falls back to `ADMIN_TOKEN`).
30
+ `Authorization: Bearer <INSIKA_GATEWAY_TOKEN>` (which falls back to `ADMIN_TOKEN`).
31
31
  The check runs in the router, **before** any dispatch, against an **allowlist** of
32
32
  public routes — so a route added later is closed until someone deliberately publishes
33
33
  it. Only these answer without a token:
@@ -171,7 +171,7 @@ LLM calls**. A *resumed* turn is never re-counted.
171
171
  > `0` to explicitly disable. A malformed value in the Studio raises a validation
172
172
  > error rather than silently disabling a production limit.
173
173
 
174
- See [Agents §Layer 4](AGENTS.md#layer-4-edge-limits-flood-and-spend-control).
174
+ See [Agents §Layer 4](POLICY.md#layer-4-edge-limits-flood-and-spend-control).
175
175
 
176
176
  ## Guardrails
177
177
 
@@ -203,7 +203,7 @@ Strictness selects the detector categories (`low` = injection only; `medium`
203
203
  (default) and `high` add sexual and abuse). Safe-reply lookup falls back per
204
204
  category: the agent's category reply → the agent's default → the builtin
205
205
  category → the builtin default. All of it is editable in the Studio Configuration
206
- form. See [Agents §Layer 3](AGENTS.md#layer-3-guardrails-content-safety).
206
+ form. See [Agents §Layer 3](POLICY.md#layer-3-guardrails-content-safety).
207
207
 
208
208
  ## Human approval
209
209
 
@@ -367,7 +367,7 @@ rotated key or a typo never takes the whole service down. The `insika doctor`
367
367
  command runs the same checks on demand against a live database. See
368
368
  [Deploy](DEPLOY.md#strict-config-and-insika-doctor).
369
369
 
370
- ## Memory and the right to be forgotten (LGPD, RFC-0031)
370
+ ## Memory and the right to be forgotten (LGPD)
371
371
 
372
372
  Memory is scoped per **`(tenant, customer)`** — the cell `"memory:<tenant>:<customer>"`
373
373
  is the isolation boundary. A query against one tenant never touches another's
@@ -394,7 +394,7 @@ forgotten or aged out. Three operations enforce the right to be forgotten:
394
394
  outbox deliveries). The operator-mutation audit store records a digest-free line
395
395
  ("a purge happened, with N records") — content-free by construction.
396
396
 
397
- ### Distilled facts are personal data (RFC-0034)
397
+ ### Distilled facts are personal data
398
398
 
399
399
  The distillation loop ([Facts](FACTS.md)) writes **proposals** — a distilled
400
400
  fact's name and value are personal data, and they are treated like the rest of
@@ -402,7 +402,7 @@ the memory footprint: `forget_customer` deletes the person's proposals (every
402
402
  status), `delete_tenant_data` deletes a tenant's, and the `retention_days`
403
403
  sweep ages them out with the transcripts they were distilled from. Provenance
404
404
  holds: an approved fact is written with `origin: "distilled:<session_ref>"`
405
- (the RFC-0031 closed set gains one spelling, never an open string). Events and
405
+ (the closed set gains one spelling, never an open string). Events and
406
406
  audit carry ids and counts only — a fact value never enters the stream, the
407
407
  ledger or a log; the evidence excerpt is a link read from the transcript at
408
408
  request time, never a copy.
@@ -412,7 +412,7 @@ deployment (in `single_tenant` the bare cell is the designed customer shape), an
412
412
  never the session-marked cells. See [Context](CONTEXT.md#memory) and
413
413
  [Deploy](DEPLOY.md#strict-config-and-insika-doctor).
414
414
 
415
- ### Harvest candidates are derived data (RFC-0035)
415
+ ### Harvest candidates are derived data
416
416
 
417
417
  The harvest loop ([Harvest](HARVEST.md)) writes **candidates** — a mined skill
418
418
  proposal is behavior instructions, the same trust level as any skill content,
@@ -428,6 +428,22 @@ product claim the origin sessions' evidence ledger did not see. Events and the
428
428
  promotion log carry ids, refs and verdicts only — a skill body never enters
429
429
  the stream.
430
430
 
431
+ ### Learned knowledge is redacted before it is stored
432
+
433
+ The [Knowledge](KNOWLEDGE.md) loop writes **concepts** extracted from finished
434
+ conversations — store-scoped, not customer-scoped, but written from real
435
+ transcript text, so the write path is deliberately conservative: a concept's
436
+ body goes through the same PII/secret redactor the output guardrail uses
437
+ before it is ever persisted, and `sources` holds session ids only — never
438
+ message content, never a customer identifier. This is the one write path in
439
+ the engine where a model-authored field is rejected outright rather than
440
+ merely validated: `provenance`, `confidence`, `sources` and the timestamps are
441
+ stamped by the engine, so a model cannot self-assign trust it did not earn or
442
+ smuggle a scope into a store-wide record. A repeat sighting that contradicts
443
+ what's on record is never silently merged — the conservative default when
444
+ the engine cannot tell is to flag it for a human, not to guess. Events carry
445
+ names and counts only — a concept's content never enters the stream.
446
+
431
447
  ## See also
432
448
 
433
449
  - [Agents](AGENTS.md) — the five access layers per agent.
data/docs/SKILLS.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Skills
3
- parent: Build an agent
4
- nav_order: 3
3
+ parent: Core concepts
4
+ nav_order: 4
5
5
  permalink: /skills/
6
6
  ---
7
7
 
@@ -16,6 +16,14 @@ while paying for the text of only the ones it opens.
16
16
 
17
17
  See [`examples/skills/`](https://github.com/guizaols/insika/tree/main/examples/skills/) for a runnable one.
18
18
 
19
+ **Skills vs. Knowledge.** They share a format — YAML frontmatter over a
20
+ Markdown body, progressive loading — which makes them easy to confuse. A
21
+ skill is **curated**: a human writes it, and it is canonical until a human
22
+ changes it. A [Knowledge](KNOWLEDGE.md) concept is **learned**: the engine
23
+ extracts it from finished conversations, it is `provenance: observed`, and it
24
+ sits below skills in the context priority ladder — earned trust, not
25
+ authored trust.
26
+
19
27
  ## Format
20
28
 
21
29
  ```markdown
@@ -281,4 +289,5 @@ reading the doctor:
281
289
  - [Agents](AGENTS.md) — the skills allowlist.
282
290
  - [Tools](TOOLS.md) — `load_skill` and deferred-tool progressive disclosure.
283
291
  - [Plugins](PLUGINS.md) — shipping skills inside a plugin, and the two extension tiers.
292
+ - [Knowledge](KNOWLEDGE.md) — the learned counterpart: engine-extracted concepts, same format, earned trust.
284
293
  - [`examples/skills/`](https://github.com/guizaols/insika/tree/main/examples/skills/) — progressive loading, runnable.
data/docs/SOAK.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Soak
3
- parent: Operate & prove it
4
- nav_order: 4
3
+ parent: Operate
4
+ nav_order: 5
5
5
  permalink: /soak/
6
6
  ---
7
7
 
@@ -88,7 +88,7 @@ insika soak --dry-run --envelope soak-envelope.md
88
88
  # every precondition, and nothing else
89
89
  insika soak --preflight --envelope soak-envelope.md
90
90
 
91
- # the run itself (INSIKA_URL + OPENCLAW_GATEWAY_TOKEN, like loadtest.rb)
91
+ # the run itself (INSIKA_URL + INSIKA_GATEWAY_TOKEN, like loadtest.rb)
92
92
  INSIKA_URL=https://<target> insika soak --run --envelope soak-envelope.md --out soak-out/
93
93
 
94
94
  # resume after a short outage (the gap is recorded and counts against the window)
data/docs/TEMPLATES.md ADDED
@@ -0,0 +1,134 @@
1
+ ---
2
+ title: Templates
3
+ parent: Integrate
4
+ nav_order: 6
5
+ permalink: /templates/
6
+ ---
7
+
8
+ # Templates
9
+
10
+ Example agents shipped **inside the gem** — `lib/insika/templates/<name>/`,
11
+ one DSL file per template. `insika new <name>` copies it for you to run and
12
+ edit; the same file is what the Studio gallery evaluates to create the
13
+ agent from a click. One source of truth, two doors — never a parallel pack
14
+ format to drift.
15
+
16
+ ## The gallery
17
+
18
+ ```bash
19
+ insika new --list
20
+ ```
21
+
22
+ ```
23
+ travel-planner Starter Weather + currency data-tools against keyless public APIs …
24
+ research-analyst Advanced Insika.system fan-out — three specialist subagents research …
25
+ daily-digest Always-on A recurring schedule plus save_artifact build and publish …
26
+ review-panel Teams Two specialists reviewed in parallel by a synthesizing lead …
27
+ repo-explorer MCP Live MCP tool-loop over http — answers questions about any …
28
+ browser-agent MCP Live MCP tool-loop over stdio — navigates and summarizes …
29
+ ```
30
+
31
+ ```bash
32
+ insika new travel-planner # copies ./travel-planner/{agent.rb,README.md}
33
+ insika new travel-planner my-trip # ...into ./my-trip/ instead
34
+ ```
35
+
36
+ The CLI prints the exact run line, including any env the template needs
37
+ **set** (not just available as an override) — a stdio MCP template needs
38
+ `INSIKA_MCP_STDIO=1`, for instance. The generated script *is* the editing
39
+ surface: no Gemfile, no questionnaire, no placeholders to fill in.
40
+
41
+ The same roster appears as a "+ from template" gallery on the Studio
42
+ `/studio/agents` page — clicking **Create** dispatches the identical
43
+ `:create_agent` (and, for a system template, one per agent) the CLI-run
44
+ copy would produce. A template marked `studio: false` in its frontmatter
45
+ (none in wave 1) shows a "CLI-only for now" note instead of a button —
46
+ reserved for a template whose value is a durable workflow, until workflow
47
+ import into a running store exists.
48
+
49
+ ## The MCP trail: point it at your own server
50
+
51
+ `repo-explorer` (http) and `browser-agent` (stdio) are not showcases for
52
+ one MCP vendor — they demonstrate exactly how to plug **any** MCP server
53
+ into an agent. Each ships with a working, keyless default so
54
+ `insika new` + the run line works with zero setup, but the server is a
55
+ config value:
56
+
57
+ ```bash
58
+ MCP_URL=https://your-mcp-server/mcp DEEPSEEK_API_KEY=sk-... ruby repo-explorer/agent.rb "..."
59
+ MCP_COMMAND=your-mcp-server INSIKA_MCP_STDIO=1 DEEPSEEK_API_KEY=sk-... ruby browser-agent/agent.rb "..."
60
+ ```
61
+
62
+ Swap the env var, rewrite the instructions for the new server's tools —
63
+ nothing else in `agent.rb` changes.
64
+
65
+ ## Writing a template
66
+
67
+ A template is `lib/insika/templates/<name>/agent.rb` + `README.md`.
68
+
69
+ **The frontmatter contract** — a `# ---` … `# ---` comment block, YAML
70
+ inside, right after the standard `# frozen_string_literal: true` (that
71
+ magic comment is skipped automatically — a template doesn't have to break
72
+ the convention every other file in the gem follows):
73
+
74
+ ```ruby
75
+ # frozen_string_literal: true
76
+
77
+ # ---
78
+ # title: My Template
79
+ # trail: Starter | Advanced | Always-on | Teams | MCP
80
+ # description: one line, shown in the CLI list and the Studio card.
81
+ # capabilities: comma, separated, tags
82
+ # studio: true # optional, default true
83
+ # env: SOME_REQUIRED_VAR # optional — env the run line must SET, not just may override
84
+ # requires: Node.js and npm # optional — a local dependency beyond the gem + a provider key
85
+ # ---
86
+ ```
87
+
88
+ **The two-doors mechanics**, in the file itself:
89
+
90
+ 1. `require "insika"` — gem-style, never `require_relative` (the file gets
91
+ copied out of the gem into an arbitrary directory).
92
+ 2. Build the agent/system as a normal top-level local: `travel = Insika.agent(...) { ... }`.
93
+ 3. Guard the CLI demo footer: `if __FILE__ == $PROGRAM_NAME ... end`. False
94
+ whenever `Insika::Templates.evaluate` loads the file (never true from
95
+ inside the gem/Studio process), so the Studio door never makes a network
96
+ call, prints anything, or parses `ARGV`.
97
+ 4. End the file with the **bare** built value (`travel`, `panel`, `team`,
98
+ …) as its last expression — `evaluate` runs the file in an isolated
99
+ `instance_eval` and returns whatever that last expression is. No
100
+ registration call, no second format.
101
+ 5. **No top-level constants.** `instance_eval`'s isolation keeps local
102
+ variables and `def`s from leaking into the NEXT template evaluated in
103
+ the same process, but Ruby scopes a `CONST = ...` assignment lexically,
104
+ not by `self` — it would leak. Use a local variable (closures see it
105
+ fine from inside a `do...end` block) — every wave-1 template does.
106
+
107
+ **Engine-only rules** (enforced by the lint below):
108
+ provider-agnostic (one provider key), zero tenant/store data, external
109
+ calls only to keyless public APIs, every tool/mcp group covered by an
110
+ explicit allowlist.
111
+
112
+ **Declaring an `mcp` server** auto-adds `"mcp:<name>"` to that agent's
113
+ `tools_allow_groups` (`Insika::DSL::Builder#mcp`) — without it the agent
114
+ could never call the MCP tool it just declared, since `PackImporter`
115
+ forces `tools_allow: []` for a pack with no `data_tool`. A **system**-level
116
+ `mcp` (declared outside any member `agent { }` block) grants no agent
117
+ access by itself — declare it inside the specific agent that needs it.
118
+
119
+ ## The E3 lint
120
+
121
+ `spec/insika/templates_spec.rb` iterates `Insika::Templates.all` for real —
122
+ one example per template name, so a broken new template fails by name, not
123
+ a generic loop assertion. It checks, per template:
124
+
125
+ - evaluates cleanly to schema-valid pack(s) (`id`/`model` present);
126
+ - every `data_tool` it declares is in that SAME pack's `tools_allow`;
127
+ - every `mcp` instance's group is granted by SOME agent in the pack(s);
128
+ - every referenced host (`data_tool` URL, http/sse `mcp` URL) passes
129
+ `Insika::EgressGuard.violation` — public HTTPS only, same guard a live
130
+ turn would apply;
131
+ - no hardcoded secret-shaped literal (`sk-...`, a long `Bearer ...` token)
132
+ in the source.
133
+
134
+ Run it before adding a template: `bundle exec rspec spec/insika/templates_spec.rb`.