insika 0.1.0 → 0.3.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 (280) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +199 -5
  3. data/README.md +8 -2
  4. data/bin/insika +231 -13
  5. data/docs/AGENTS.md +505 -6
  6. data/docs/API.md +56 -0
  7. data/docs/CHANNELS.md +100 -10
  8. data/docs/CONTEXT.md +147 -19
  9. data/docs/DEPLOY.md +34 -11
  10. data/docs/EMBEDDING.md +11 -7
  11. data/docs/EVALS.md +20 -1
  12. data/docs/FACTS.md +135 -0
  13. data/docs/HARVEST.md +117 -0
  14. data/docs/LOADTEST.md +17 -10
  15. data/docs/OBSERVABILITY.md +65 -2
  16. data/docs/REFINEMENT.md +9 -9
  17. data/docs/RELEASING.md +34 -7
  18. data/docs/RUNNING-LOCAL.md +4 -4
  19. data/docs/SECURITY.md +85 -11
  20. data/docs/SKILLS.md +189 -3
  21. data/docs/SOAK.md +127 -0
  22. data/docs/TOOLS.md +70 -2
  23. data/docs/WHY.md +1 -1
  24. data/docs/WORKFLOWS.md +2 -2
  25. data/docs/domain.md +115 -0
  26. data/docs/index.md +2 -2
  27. data/docs/onboarding/start.md +1 -1
  28. data/lib/insika/agent_profile.rb +228 -26
  29. data/lib/insika/alert_dispatcher.rb +139 -0
  30. data/lib/insika/balloon_splitter.rb +102 -0
  31. data/lib/insika/baseline_store.rb +2 -2
  32. data/lib/insika/budget_ledger.rb +166 -0
  33. data/lib/insika/cache_series_store.rb +49 -0
  34. data/lib/insika/channel_delivery.rb +132 -24
  35. data/lib/insika/channel_registry.rb +1 -1
  36. data/lib/insika/channels/relay.rb +80 -6
  37. data/lib/insika/channels/web/widget.js +2 -2
  38. data/lib/insika/channels/web.rb +9 -9
  39. data/lib/insika/channels/webhook.rb +58 -0
  40. data/lib/insika/chat_builder.rb +145 -13
  41. data/lib/insika/checkpoint_store.rb +16 -0
  42. data/lib/insika/circuit_state.rb +114 -0
  43. data/lib/insika/coercion.rb +8 -0
  44. data/lib/insika/commands/agent_payload.rb +6 -4
  45. data/lib/insika/commands/cancel_followup.rb +49 -0
  46. data/lib/insika/commands/create_agent.rb +2 -2
  47. data/lib/insika/commands/create_session.rb +1 -1
  48. data/lib/insika/commands/delete_llm_provider.rb +1 -1
  49. data/lib/insika/commands/delete_skill.rb +43 -0
  50. data/lib/insika/commands/delete_tenant_data.rb +95 -0
  51. data/lib/insika/commands/export_customer_memory.rb +48 -0
  52. data/lib/insika/commands/forget_customer.rb +117 -0
  53. data/lib/insika/commands/freeze_funnel_baseline.rb +113 -0
  54. data/lib/insika/commands/gate_harvest.rb +138 -0
  55. data/lib/insika/commands/gate_refinement.rb +12 -12
  56. data/lib/insika/commands/import_mcp_tools.rb +1 -1
  57. data/lib/insika/commands/import_tools.rb +4 -4
  58. data/lib/insika/commands/issue_tenant_token.rb +41 -0
  59. data/lib/insika/commands/judge_shadow_pairs.rb +124 -0
  60. data/lib/insika/commands/memory_forget_fact.rb +20 -4
  61. data/lib/insika/commands/memory_put_fact.rb +23 -4
  62. data/lib/insika/commands/promote_harvest.rb +130 -0
  63. data/lib/insika/commands/record_outcome.rb +46 -0
  64. data/lib/insika/commands/record_shadow_reply.rb +68 -0
  65. data/lib/insika/commands/reject_harvest.rb +38 -0
  66. data/lib/insika/commands/resolve_proposal.rb +108 -0
  67. data/lib/insika/commands/resolve_refinement.rb +1 -1
  68. data/lib/insika/commands/revoke_contact.rb +49 -0
  69. data/lib/insika/commands/revoke_token.rb +39 -0
  70. data/lib/insika/commands/rollback_harvest.rb +86 -0
  71. data/lib/insika/commands/rotate_tenant_token.rb +43 -0
  72. data/lib/insika/commands/run_distillation.rb +186 -0
  73. data/lib/insika/commands/run_harvest.rb +393 -0
  74. data/lib/insika/commands/run_refinement.rb +5 -5
  75. data/lib/insika/commands/send_message.rb +112 -15
  76. data/lib/insika/commands/session_purge.rb +67 -0
  77. data/lib/insika/commands/set_agent_tools.rb +1 -1
  78. data/lib/insika/commands/set_skill_agents.rb +60 -19
  79. data/lib/insika/commands/trigger_workflow.rb +1 -1
  80. data/lib/insika/commands/update_agent.rb +1 -1
  81. data/lib/insika/commands/write_data_tool.rb +1 -1
  82. data/lib/insika/commands/write_golden.rb +1 -1
  83. data/lib/insika/commands/write_skill.rb +19 -9
  84. data/lib/insika/config_store.rb +8 -4
  85. data/lib/insika/contact_store.rb +183 -0
  86. data/lib/insika/context/builder.rb +23 -5
  87. data/lib/insika/context/fragment.rb +31 -3
  88. data/lib/insika/context/priority.rb +6 -2
  89. data/lib/insika/context/provider.rb +17 -3
  90. data/lib/insika/context/providers/briefing.rb +96 -0
  91. data/lib/insika/context/providers/memory.rb +16 -7
  92. data/lib/insika/context/providers/prompt.rb +30 -2
  93. data/lib/insika/context/providers/request.rb +1 -1
  94. data/lib/insika/context/providers/session.rb +17 -2
  95. data/lib/insika/context/providers/skill.rb +7 -1
  96. data/lib/insika/context/providers/skill_trigger.rb +128 -0
  97. data/lib/insika/context/providers/tool_search.rb +2 -0
  98. data/lib/insika/context_trace_store.rb +128 -0
  99. data/lib/insika/delegation_store.rb +2 -2
  100. data/lib/insika/distill.rb +224 -0
  101. data/lib/insika/distill_engine.rb +169 -0
  102. data/lib/insika/doctor.rb +962 -7
  103. data/lib/insika/dsl/runtime.rb +20 -11
  104. data/lib/insika/dsl/server_boot.rb +74 -4
  105. data/lib/insika/dsl/system.rb +1 -1
  106. data/lib/insika/dsl.rb +152 -15
  107. data/lib/insika/edge_limiter.rb +167 -8
  108. data/lib/insika/egress_guard.rb +3 -3
  109. data/lib/insika/env_schema.rb +22 -12
  110. data/lib/insika/errors.rb +72 -5
  111. data/lib/insika/evals/assertions.rb +15 -14
  112. data/lib/insika/evals/baseline.rb +3 -3
  113. data/lib/insika/evals/golden.rb +8 -8
  114. data/lib/insika/evals/judge.rb +7 -7
  115. data/lib/insika/evals/pairwise.rb +21 -9
  116. data/lib/insika/evals/report.rb +2 -2
  117. data/lib/insika/evals/runner.rb +6 -6
  118. data/lib/insika/evals/transport.rb +2 -2
  119. data/lib/insika/event_stream.rb +23 -5
  120. data/lib/insika/evidence.rb +183 -0
  121. data/lib/insika/executor.rb +1092 -160
  122. data/lib/insika/followup_engine.rb +207 -0
  123. data/lib/insika/followup_policy.rb +221 -0
  124. data/lib/insika/followup_store.rb +306 -0
  125. data/lib/insika/frontmatter.rb +1 -1
  126. data/lib/insika/funnel_declaration.rb +106 -0
  127. data/lib/insika/funnel_fold.rb +179 -0
  128. data/lib/insika/funnel_store.rb +163 -0
  129. data/lib/insika/golden_store.rb +3 -3
  130. data/lib/insika/grounding/matcher.rb +69 -0
  131. data/lib/insika/grounding.rb +44 -0
  132. data/lib/insika/harvest/conversion_gate.rb +159 -0
  133. data/lib/insika/harvest/criterion.rb +98 -0
  134. data/lib/insika/harvest/gate.rb +194 -0
  135. data/lib/insika/harvest/negative_list.rb +199 -0
  136. data/lib/insika/harvest.rb +241 -0
  137. data/lib/insika/harvest_engine.rb +193 -0
  138. data/lib/insika/harvest_store.rb +548 -0
  139. data/lib/insika/http_client.rb +3 -3
  140. data/lib/insika/inbound_log.rb +1 -1
  141. data/lib/insika/llm_configurator.rb +3 -3
  142. data/lib/insika/loop_detector.rb +143 -0
  143. data/lib/insika/mcp_http_client.rb +4 -4
  144. data/lib/insika/mcp_tool_ingestor.rb +6 -6
  145. data/lib/insika/media.rb +298 -0
  146. data/lib/insika/memory_audit_store.rb +85 -0
  147. data/lib/insika/memory_store.rb +264 -23
  148. data/lib/insika/message_origin.rb +8 -3
  149. data/lib/insika/model_resolver.rb +1 -1
  150. data/lib/insika/model_selection.rb +5 -4
  151. data/lib/insika/model_visible.rb +87 -0
  152. data/lib/insika/model_visible_trace_store.rb +66 -0
  153. data/lib/insika/onboarding.rb +8 -3
  154. data/lib/insika/outbox_store.rb +44 -6
  155. data/lib/insika/outcome_store.rb +147 -0
  156. data/lib/insika/overlay_tool_registry.rb +3 -4
  157. data/lib/insika/pack.rb +3 -3
  158. data/lib/insika/pack_importer.rb +17 -15
  159. data/lib/insika/packaging.rb +163 -0
  160. data/lib/insika/parity/criterion.rb +79 -0
  161. data/lib/insika/parity/verdict.rb +318 -0
  162. data/lib/insika/pending_action_store.rb +1 -1
  163. data/lib/insika/plugin/loader.rb +2 -2
  164. data/lib/insika/policy/policy.rb +1 -1
  165. data/lib/insika/prefix_fingerprint.rb +58 -0
  166. data/lib/insika/profile_source.rb +34 -7
  167. data/lib/insika/proposal_store.rb +271 -0
  168. data/lib/insika/provider_error_classifier.rb +160 -0
  169. data/lib/insika/queue_policy.rb +6 -3
  170. data/lib/insika/recovery.rb +47 -6
  171. data/lib/insika/refinement/candidate.rb +4 -4
  172. data/lib/insika/refinement/evidence_collector.rb +6 -6
  173. data/lib/insika/refinement/gate.rb +7 -7
  174. data/lib/insika/refinement/panel.rb +7 -7
  175. data/lib/insika/refinement/proposer.rb +10 -10
  176. data/lib/insika/refinement_store.rb +12 -12
  177. data/lib/insika/reliability.rb +211 -0
  178. data/lib/insika/retention.rb +281 -0
  179. data/lib/insika/routing.rb +101 -0
  180. data/lib/insika/safety/config.rb +46 -6
  181. data/lib/insika/safety/corpus.rb +255 -0
  182. data/lib/insika/safety/detectors.rb +34 -115
  183. data/lib/insika/safety/factory.rb +18 -5
  184. data/lib/insika/safety/grounding_enforcer.rb +59 -0
  185. data/lib/insika/safety/grounding_validator.rb +49 -0
  186. data/lib/insika/safety/input_guardrail.rb +20 -5
  187. data/lib/insika/safety/moderator.rb +19 -11
  188. data/lib/insika/safety/output_filter.rb +10 -6
  189. data/lib/insika/safety/output_validator.rb +13 -7
  190. data/lib/insika/safety/safe_responses.rb +1 -1
  191. data/lib/insika/sandbox/boundary.rb +2 -2
  192. data/lib/insika/sandbox.rb +1 -1
  193. data/lib/insika/schema_guard.rb +35 -0
  194. data/lib/insika/server/app.rb +366 -54
  195. data/lib/insika/server/boot.rb +4 -4
  196. data/lib/insika/server/rack_app.rb +31 -7
  197. data/lib/insika/server/responses.rb +58 -9
  198. data/lib/insika/server/tenant_auth.rb +61 -0
  199. data/lib/insika/session_actor.rb +11 -7
  200. data/lib/insika/session_store.rb +66 -3
  201. data/lib/insika/settings_store.rb +15 -5
  202. data/lib/insika/shadow_pair_store.rb +258 -0
  203. data/lib/insika/shutdown.rb +4 -4
  204. data/lib/insika/skill_catalog.rb +131 -20
  205. data/lib/insika/skill_store.rb +70 -22
  206. data/lib/insika/soak/envelope.rb +140 -0
  207. data/lib/insika/soak/report.rb +392 -0
  208. data/lib/insika/soak/runner.rb +554 -0
  209. data/lib/insika/steer_injector.rb +1 -1
  210. data/lib/insika/store.rb +11 -2
  211. data/lib/insika/stores/memory.rb +6 -0
  212. data/lib/insika/stores/sqlite.rb +8 -0
  213. data/lib/insika/studio/app.rb +1058 -75
  214. data/lib/insika/studio/assets/dist/application.css +1 -1
  215. data/lib/insika/studio/assets/dist/application.js +27 -26
  216. data/lib/insika/studio/assets/dist/favicon.svg +6 -0
  217. data/lib/insika/studio/forms.rb +274 -22
  218. data/lib/insika/studio/nav_icons.rb +7 -2
  219. data/lib/insika/studio/views/_message.erb +2 -2
  220. data/lib/insika/studio/views/agent_detail.erb +629 -86
  221. data/lib/insika/studio/views/agents.erb +11 -7
  222. data/lib/insika/studio/views/approvals.erb +4 -1
  223. data/lib/insika/studio/views/chats.erb +4 -1
  224. data/lib/insika/studio/views/customer.erb +94 -0
  225. data/lib/insika/studio/views/customers.erb +32 -0
  226. data/lib/insika/studio/views/evals.erb +4 -1
  227. data/lib/insika/studio/views/facts.erb +133 -0
  228. data/lib/insika/studio/views/followups.erb +125 -0
  229. data/lib/insika/studio/views/funnel.erb +106 -0
  230. data/lib/insika/studio/views/harvest.erb +234 -0
  231. data/lib/insika/studio/views/home.erb +2 -1
  232. data/lib/insika/studio/views/layout.erb +1 -0
  233. data/lib/insika/studio/views/parity.erb +147 -0
  234. data/lib/insika/studio/views/playground.erb +7 -1
  235. data/lib/insika/studio/views/refinement.erb +4 -4
  236. data/lib/insika/studio/views/session.erb +133 -3
  237. data/lib/insika/studio/views/settings.erb +9 -12
  238. data/lib/insika/studio/views/skills.erb +66 -12
  239. data/lib/insika/studio/views/system_files.erb +1 -1
  240. data/lib/insika/studio/views/task.erb +13 -0
  241. data/lib/insika/studio/views/tasks.erb +4 -1
  242. data/lib/insika/studio/views/tools.erb +0 -1
  243. data/lib/insika/subagent_graph.rb +3 -3
  244. data/lib/insika/task_actor.rb +3 -3
  245. data/lib/insika/task_store.rb +22 -2
  246. data/lib/insika/telemetry/pricing.rb +3 -3
  247. data/lib/insika/telemetry/recorder.rb +1 -1
  248. data/lib/insika/telemetry.rb +2 -2
  249. data/lib/insika/testing/store_contract.rb +54 -33
  250. data/lib/insika/tick.rb +146 -0
  251. data/lib/insika/token_store.rb +168 -0
  252. data/lib/insika/tool_assembly.rb +5 -5
  253. data/lib/insika/tool_definition.rb +25 -15
  254. data/lib/insika/tool_envelope.rb +70 -1
  255. data/lib/insika/tool_manifest.rb +11 -7
  256. data/lib/insika/tool_output_compressor.rb +100 -0
  257. data/lib/insika/tool_store.rb +1 -1
  258. data/lib/insika/tool_trace_store.rb +1 -1
  259. data/lib/insika/tools/concurrency.rb +2 -2
  260. data/lib/insika/tools/data_defined_tool.rb +14 -5
  261. data/lib/insika/tools/generate_image.rb +44 -0
  262. data/lib/insika/tools/load_skill.rb +61 -3
  263. data/lib/insika/tools/schedule_followup.rb +164 -0
  264. data/lib/insika/tools/stuck_signal.rb +44 -0
  265. data/lib/insika/tools/subagent.rb +4 -4
  266. data/lib/insika/tools/subagents.rb +1 -1
  267. data/lib/insika/tools/tts.rb +47 -0
  268. data/lib/insika/tools/update_briefing.rb +126 -0
  269. data/lib/insika/turn_output.rb +2 -2
  270. data/lib/insika/turn_state.rb +54 -13
  271. data/lib/insika/turn_timing.rb +24 -4
  272. data/lib/insika/usage_ledger.rb +1 -1
  273. data/lib/insika/version.rb +1 -1
  274. data/lib/insika/vitals.rb +84 -0
  275. data/lib/insika/wiring/graph.rb +372 -34
  276. data/lib/insika/workflow.rb +1 -1
  277. data/lib/insika/workflow_registry.rb +1 -1
  278. data/lib/insika.rb +122 -16
  279. metadata +95 -2
  280. data/lib/insika/server/admin_auth.rb +0 -29
data/docs/SECURITY.md CHANGED
@@ -47,16 +47,26 @@ dispatch **any** registered authoring Command (`write_agent_file`, `write_data_t
47
47
  `upsert_llm_provider`, `update_settings`, `delete_agent`). Treat that token as
48
48
  operator-grade — it is not a read key, and a leak is agent takeover, not just usage.
49
49
 
50
- > **The auth decision (RFC-0018, decided for 0.1.0): one deployment, one token;
51
- > multi-tenancy belongs to the host.** There is exactly one credential per
52
- > surface — `OPENCLAW_GATEWAY_TOKEN` for `/v1`, `ADMIN_TOKEN` for the Studio —
53
- > and no per-caller scoping inside the engine. That is what the code has always
54
- > done; this paragraph is the decision written down so nothing built on top
55
- > inherits an assumption nobody stated. Serving two tenants from one mount means
56
- > putting the app behind your own authentication (see
57
- > [Embedding](EMBEDDING.md#embedding-is-not-multi-tenancy)), and scoped auth —
58
- > one mount, several tokens with different powers — is a real RFC the day an
59
- > adopter needs it, not a someday.
50
+ ### Multi-tenant mode (`INSIKA_TENANCY=multi_tenant`)
51
+
52
+ A single operator-grade token cannot host N stores. With `INSIKA_TENANCY=multi_tenant`
53
+ the Bearer is resolved to a **principal** before the routes:
54
+
55
+ - **Per-tenant tokens** (`POST /v1/commands/issue_tenant_token`) scope a caller to
56
+ one tenant; **operator** tokens (and the legacy gateway token — an existing
57
+ deployment switching modes keeps its credential) have the run of the deployment.
58
+ - Tokens are stored **only as SHA-256 hashes**; the plaintext is shown exactly once,
59
+ at issue time. Rotation (`rotate_tenant_token`) and revocation (`revoke_token`)
60
+ are operator commands — revoking one tenant's token never touches another's.
61
+ - A tenant principal reaches only its **own runtime surfaces** (`/v1/sessions`,
62
+ `/v1/messages`, `/v1/responses`, workflow runs, and its own session/task/event
63
+ reads). Every authoring/provisioning/config surface answers `403` to a tenant.
64
+ - Isolation is the key, not a convention: a tenant's sessions live under
65
+ `<tenant>:<session-id>`, its commands carry `meta.tenant` (memory scoping,
66
+ event tagging), and reading another tenant's session/task reads as `404`.
67
+
68
+ `single_tenant` (the default) is exactly the classic behavior above — one
69
+ operator credential, no principal, no stamping.
60
70
 
61
71
  ## The `/v1` contract is versioned by date
62
72
 
@@ -172,7 +182,10 @@ moderator off. See [`examples/guardrails/`](https://github.com/guizaols/insika/t
172
182
  - **Input** — deterministic detectors (prompt-injection, abuse) run *before* the
173
183
  model. A flagged input gets a **safe refusal without burning a model turn** — an
174
184
  injection or a flood never reaches the provider. An LLM moderator can be layered
175
- on top.
185
+ on top. The moderator is **fail-open**: an error or an unparseable reply never
186
+ blocks a legitimate customer — but silence is not a negative. That third state
187
+ surfaces as a `:guardrail_flagged` event with category `moderator_unavailable`,
188
+ so a degraded tier is distinguishable from a healthy one in the audit stream.
176
189
  - **Output** — moderation plus PII/secret redaction on the streamed response, and
177
190
  a post-turn validator.
178
191
 
@@ -354,6 +367,67 @@ rotated key or a typo never takes the whole service down. The `insika doctor`
354
367
  command runs the same checks on demand against a live database. See
355
368
  [Deploy](DEPLOY.md#strict-config-and-insika-doctor).
356
369
 
370
+ ## Memory and the right to be forgotten (LGPD, RFC-0031)
371
+
372
+ Memory is scoped per **`(tenant, customer)`** — the cell `"memory:<tenant>:<customer>"`
373
+ is the isolation boundary. A query against one tenant never touches another's
374
+ cells. A turn with no customer falls back to the session's own **marked** cell
375
+ (`"memory:chat:<session id>"`) — the marker keeps the Studio drill and the doctor
376
+ from ever reading a conversation as a customer (a bare `memory:<id>` cell is
377
+ indistinguishable from a single-tenant customer ref). Cells written before the
378
+ marker (bare session ids) may still appear as customers in the drill until
379
+ forgotten or aged out. Three operations enforce the right to be forgotten:
380
+
381
+ - **Retention** — the `memory_ttl_days` setting (platform default per cell, or an
382
+ ops-authored per-tenant map) sweeps facts older than the window. A per-fact
383
+ `expires_at` override wins over the cell TTL. Both run on the daily retention
384
+ tick, **not** gated by `retention_days` (the conversation-footprint window).
385
+ Note the interaction: an explicit `expires_at` also exempts the fact from the
386
+ `retention_days` age-based sweep — an explicit date owns that fact's life, so a
387
+ far-future date makes the fact immune to *both* age passes. Export and forget
388
+ still see it; the only way to remove it early is the Customers drill.
389
+ - **Export** — `export_customer_memory` returns the full fact + note content to the
390
+ **operator** (the Studio turns it into a JSON download). The emitted event carries
391
+ counts only — the event stream, the audit store and every log stay content-free.
392
+ - **Forget** — `forget_customer` purges the customer's memory cell AND their
393
+ sessions (and everything those sessions left behind: traces, tasks, checkpoints,
394
+ outbox deliveries). The operator-mutation audit store records a digest-free line
395
+ ("a purge happened, with N records") — content-free by construction.
396
+
397
+ ### Distilled facts are personal data (RFC-0034)
398
+
399
+ The distillation loop ([Facts](FACTS.md)) writes **proposals** — a distilled
400
+ fact's name and value are personal data, and they are treated like the rest of
401
+ the memory footprint: `forget_customer` deletes the person's proposals (every
402
+ status), `delete_tenant_data` deletes a tenant's, and the `retention_days`
403
+ sweep ages them out with the transcripts they were distilled from. Provenance
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
406
+ audit carry ids and counts only — a fact value never enters the stream, the
407
+ ledger or a log; the evidence excerpt is a link read from the transcript at
408
+ request time, never a copy.
409
+
410
+ The doctor's `memory-scopes` check flags bare cells only in a `multi_tenant`
411
+ deployment (in `single_tenant` the bare cell is the designed customer shape), and
412
+ never the session-marked cells. See [Context](CONTEXT.md#memory) and
413
+ [Deploy](DEPLOY.md#strict-config-and-insika-doctor).
414
+
415
+ ### Harvest candidates are derived data (RFC-0035)
416
+
417
+ The harvest loop ([Harvest](HARVEST.md)) writes **candidates** — a mined skill
418
+ proposal is behavior instructions, the same trust level as any skill content,
419
+ and it is store-scoped, never customer-scoped: `forget_customer` does NOT reach
420
+ it (a candidate's skill body is not customer content). Candidates reference
421
+ sessions by id and their evidence excerpt is the transcript rendered at request
422
+ time through the output filter — never a copy. `delete_tenant_data` purges a
423
+ tenant's candidates, promotion rows and markers; the `retention_days` sweep
424
+ ages them out with the transcripts they were derived from (they are
425
+ re-derivable — pruning is never data loss). **No auto-application**: nothing
426
+ reaches the catalog without a human click, and the grounding filter refuses any
427
+ product claim the origin sessions' evidence ledger did not see. Events and the
428
+ promotion log carry ids, refs and verdicts only — a skill body never enters
429
+ the stream.
430
+
357
431
  ## See also
358
432
 
359
433
  - [Agents](AGENTS.md) — the five access layers per agent.
data/docs/SKILLS.md CHANGED
@@ -22,6 +22,8 @@ See [`examples/skills/`](https://github.com/guizaols/insika/tree/main/examples/s
22
22
  ---
23
23
  name: refunds # must equal the directory name
24
24
  description: When and how to process a refund # the Level-1 trigger text
25
+ triggers: [refund, money back] # optional: deterministic activation (below)
26
+ companions: [refund-policy] # optional: skills this one cannot work without
25
27
  ---
26
28
 
27
29
  <the full playbook body — loaded only on demand>
@@ -33,17 +35,121 @@ which the `name:` field must match.
33
35
  ## Progressive loading: two levels
34
36
 
35
37
  - **Level 1 — metadata only.** A context provider injects an `<available_skills>`
36
- list (the name + one-line description of every allowed skill) into the system
37
- prompt, telling the model to load a skill before acting on it. Cheap, and always
38
- present for allowed skills.
38
+ list into the system prompt — the name, the one-line description and the
39
+ `triggers:` of every allowed skill — telling the model to load a skill before
40
+ acting on it. Cheap, and always present for allowed skills. **This is the routing
41
+ table, and it is generated:** it cannot disagree with the allowlist, so do not
42
+ hand-write one in a prompt file (see [Drift guards](#drift-guards)).
39
43
  - **Level 2 — the full body.** A built-in `load_skill` tool returns the skill body
40
44
  on demand. It enforces the agent's skill allowlist and is wired **automatically**
41
45
  whenever the agent has any allowed skills — you do not add it to `tools_allow`.
46
+ - **Deterministic activation — `triggers:`.** When the user message contains one
47
+ of the skill's `triggers`, the body is injected for that turn — no model
48
+ decision, no `load_skill` call. Only matched skills, only that turn. Use it for
49
+ skills that MUST fire on known phrases; model loading stays as the fallback for
50
+ everything else. Matching is on **whole words**, case-insensitive and
51
+ accent-folded: `presente` fires on *um presente* and *presénte*, never inside
52
+ *apresente*.
42
53
 
43
54
  The Level-1 list is budgeted like any other context fragment
44
55
  (see [Context](CONTEXT.md)); the Level-2 body only costs tokens on the turns that
45
56
  open it.
46
57
 
58
+ ### Only trigger a skill that can finish the turn alone
59
+
60
+ A `triggers:` match is not a hint — the body lands in the prompt with the
61
+ authority of an instruction. So put triggers only on a skill that is
62
+ **self-sufficient** for the turn it fires on.
63
+
64
+ The failure mode is counter-intuitive: injecting a skill that is only *part* of
65
+ the answer is **worse than injecting nothing**. Give the model a reference table
66
+ whose procedure lives in a companion skill, and it now holds a plausible
67
+ half-recipe — so it never calls `load_skill` for the other half, and improvises
68
+ the missing part. A precise trigger on the wrong kind of skill still breaks the
69
+ turn.
70
+
71
+ Reference tables, vocabularies and lookup maps are the skills to leave on
72
+ level 1. Whole procedures ("run this journey", "recover from this error") are the
73
+ ones worth triggering.
74
+
75
+ ## Always-on skills: `skills_eager`
76
+
77
+ A skill that every turn needs — output format, the marker vocabulary, how to
78
+ recover from a failed tool — should not depend on the model choosing to load it.
79
+ Name it on the **agent** and its body is in the prompt on every turn:
80
+
81
+ ```ruby
82
+ Insika.agent("consultant") do
83
+ skills_eager "recommendation-formatting", "tool-error-recovery"
84
+ # skills_eager # or: every allowed skill (a corpus that fits the budget)
85
+ # skills_eager false # or: none — the default
86
+ end
87
+ ```
88
+
89
+ An eager skill also **leaves level 1**: it is absent from `<available_skills>` and
90
+ `load_skill` refuses to serve it. There is no level 2 left to fetch, and a catalog
91
+ pointing at a body already in the prompt only invites a call that pays for a
92
+ duplicate.
93
+
94
+ ### Why the agent decides, and not the skill
95
+
96
+ Eagerness used to be an `eager: true` key in the `SKILL.md` frontmatter. That put
97
+ the decision on the wrong object: **skills are shared.** `escalation-to-human`,
98
+ `recommendation-formatting` and `tool-error-recovery` each sit in several agents'
99
+ allowlists, and one flag on the skill forced one decision onto every agent holding
100
+ it — with no way to be always-on for the agent that needs it and discretionary for
101
+ the one that does not.
102
+
103
+ `skills_eager` is a per-agent list, so the same shared skill can be both. The
104
+ frontmatter key is **ignored** — `insika doctor` flags any skill still carrying it,
105
+ and names the agent setting that replaced it.
106
+
107
+ A name that is not in the agent's `skills` allowlist is a no-op (eagerness is
108
+ intersected with what the agent is allowed to see); `doctor` flags that too.
109
+
110
+ ### Keep the discretionary skills on the load path
111
+
112
+ Making everything eager is a trap, and the reason is not the tokens: **it costs you
113
+ the signal**. When every body is present on every turn, "which skills were active"
114
+ is always "all of them", and you can no longer tell which one the model reached for.
115
+ The `load_skill` call is the only record of that choice — it is a persisted tool
116
+ message, so it shows up in the transcript on its own.
117
+
118
+ So the split is: **eager for what the turn always needs, `load_skill` for what the
119
+ turn might need.** The second group is where you want the model's choice on the
120
+ record, because that is the group where a wrong choice is worth seeing.
121
+
122
+ The token trade is real but smaller than it looks: eager bodies sit at a fixed
123
+ position ahead of the history, so they belong to the **cacheable prefix**, and they
124
+ are still evictable under budget pressure, unlike the pinned identity. Conditional
125
+ injection is what breaks that prefix, on exactly the turns it fires.
126
+
127
+ ## Seeing which skills were active, and why
128
+
129
+ The load path is legible for free: `load_skill` is a tool, so the call is a
130
+ persisted message and shows up in the transcript on its own. The deterministic paths
131
+ are not a call, so the engine reports them itself — **with a reason per skill**:
132
+
133
+ | reason | what it means |
134
+ |---|---|
135
+ | `eager` | the agent's `skills_eager` names it, so every turn gets it |
136
+ | `trigger:<phrase>` | this message matched that `triggers:` entry — the phrase as **authored**, so you can find the line to edit |
137
+ | `pack` | a plugin's own context provider supplied the body |
138
+
139
+ Where it shows up, per turn, in the Studio session screen:
140
+
141
+ - an **activation card in the transcript thread**, placed at the top of its turn and
142
+ in the same visual language as a tool result, so a context-injected skill and a
143
+ model-loaded one read the same way;
144
+ - the **Context card**, next to that category's token count, for the after-the-fact
145
+ audit;
146
+ - the `skill_activated` **event** (`skills: [{name, reason}]`, `source: "context"`),
147
+ with full task/session correlation.
148
+
149
+ All three are computed from what actually reached the prompt **after the budget
150
+ cut**: a body the budget evicted is reported as an eviction, never as an activation.
151
+ A turn that mixes both paths is labelled `mixed`, and each line keeps its own reason.
152
+
47
153
  ## Where skills live: the store, over a disk seed
48
154
 
49
155
  - Skills live as **rows in SQLite** — one row per skill, holding the entire
@@ -58,6 +164,68 @@ open it.
58
164
  > runtime operation (Studio / API / DSL), not a commit. See
59
165
  > [Context](CONTEXT.md#the-volume).
60
166
 
167
+ ## Pairs that must not break: `companions:`
168
+
169
+ Injecting *part* of an answer is worse than injecting nothing. Give the model a line
170
+ map whose query-construction rules live in another skill and it holds a plausible
171
+ half-recipe — so it never calls `load_skill` for the other half, and improvises the
172
+ missing part. Measured on a real pack: the map arrived by trigger, the rules did not,
173
+ and the searches came out malformed. Twice.
174
+
175
+ Declare the dependency and it travels with whatever brought it — a trigger match, the
176
+ agent's eager set, or a `load_skill` call (which returns both bodies in the one call):
177
+
178
+ ```yaml
179
+ companions: [query-construction]
180
+ ```
181
+
182
+ Two deliberate limits:
183
+
184
+ - **One level, no transitive walk.** A cycle would be a hang and a chain a budget
185
+ blowout, and "cannot work without" is a direct relationship.
186
+ - **Never widens an allowlist.** A companion the agent is not allowed to load is
187
+ simply absent; `insika doctor` flags the declaration instead.
188
+
189
+ ## Specializing a shared skill for one agent
190
+
191
+ Skills are shared on purpose: `escalation-to-human` belongs in several agents'
192
+ allowlists. But sometimes one agent needs a different version of the same skill —
193
+ its own return policy, its own store name — and forking it under a second name
194
+ throws the sharing away and leaves two things to keep in step.
195
+
196
+ So the store has a second scope, and resolution is a **precedence chain** with one
197
+ more dimension:
198
+
199
+ ```
200
+ for agent A, skill <name>: (A, <name>) in the agent scope,
201
+ then <name> in the shared scope
202
+ ```
203
+
204
+ Three cases fall out of that one rule:
205
+
206
+ | case | what exists in the store |
207
+ |---|---|
208
+ | **shared** | only the shared record — every agent gets the same body |
209
+ | **override** | both — the agent's wins, for that agent only |
210
+ | **agent-private** | only the agent record — invisible elsewhere, and the name may collide freely |
211
+
212
+ The **name never changes.** An override keeps saying `name: escalation-to-human`
213
+ inside, because it *is* that skill, specialized; the allowlist, the `<available_skills>`
214
+ list, `load_skill` and the activation card all keep showing the bare name. What
215
+ decides which record you get is its **position in the store**, never the frontmatter —
216
+ otherwise an override would clobber the shared skill for everybody.
217
+
218
+ Write one with `agent:`, and remove it the same way (which un-specializes, leaving
219
+ the shared skill in place):
220
+
221
+ ```ruby
222
+ dispatch(:write_skill, { name: "escalation-to-human", agent: "store-demo", content: md })
223
+ dispatch(:delete_skill, { name: "escalation-to-human", agent: "store-demo" })
224
+ ```
225
+
226
+ In the Studio: **Skills → specialize for this agent**, which seeds the override from
227
+ the shared body.
228
+
61
229
  ## Making a new skill "show up"
62
230
 
63
231
  For an agent to actually use a skill, **both** conditions must hold:
@@ -89,6 +257,24 @@ Two ways to satisfy both:
89
257
  `load_skill` errors → the store row is missing or misnamed (condition 1); the
90
258
  `name:` frontmatter must equal the directory name.
91
259
 
260
+ ## Drift guards
261
+
262
+ A skill catalog drifts against the prose that routes to it, and every way it happened
263
+ on the pilot was silent — found by reading a customer conversation days later. So the
264
+ routing table is **generated** (above), and `insika doctor` reports the residue the
265
+ generator cannot remove. Every check takes mechanical inputs only — names, allowlists,
266
+ agent identities — because one false positive is enough for an operator to stop
267
+ reading the doctor:
268
+
269
+ | finding | what it means |
270
+ |---|---|
271
+ | a prompt file names a skill outside that agent's allowlist | leftover hand-written routing: the model is told to use something it cannot load |
272
+ | a shared skill's body names one of its own holders | specialized text in shared clothing — the other holders are served that store's policy as their own. Specialize it instead |
273
+ | a body references another catalog skill without declaring it a companion | the pair can still arrive apart |
274
+ | a declared companion is outside an agent's allowlist | the pair cannot travel for that agent, and the engine will not widen the allowlist |
275
+ | a skill still declares `eager:` in its frontmatter | the key is ignored; the decision moved to the agent |
276
+ | an agent marks a skill eager that it does not allow | the name is a no-op |
277
+
92
278
  ## See also
93
279
 
94
280
  - [Context](CONTEXT.md) — how the skills list is budgeted into a turn.
data/docs/SOAK.md ADDED
@@ -0,0 +1,127 @@
1
+ ---
2
+ title: Soak
3
+ parent: Operate & prove it
4
+ nav_order: 4
5
+ permalink: /soak/
6
+ ---
7
+
8
+ # SOAK — does it degrade over uptime?
9
+
10
+ The soak is the third question the measurement toolbox answers, and it is the
11
+ one the other two cannot:
12
+
13
+ | Tool | Question | Drives |
14
+ |------|----------|--------|
15
+ | [`scripts/bench.rb`](BENCHMARK.md) | How much overhead does the engine itself add? | publishable claims |
16
+ | [Load test](LOADTEST.md) | What does a burst look like, end to end? | topology + provider choice |
17
+ | **`insika soak`** | **Does the deploy degrade over 72 hours of steady load?** | the cut argument: "it does not tax uptime" |
18
+
19
+ A load test fires a wave, joins it, and reports. A soak **sustains a declared
20
+ arrival rate for days** and asks whether the *process itself* — resident memory,
21
+ per-turn local work, restarts — degrades with uptime. A leak that only shows at
22
+ hour 40 is invisible to a 10-minute bench.
23
+
24
+ ## The protocol in one paragraph
25
+
26
+ 1. **Freeze the envelope first.** A deployment-side envelope file declares the
27
+ load shape, the gated ceilings and the isolation contract — *before* the
28
+ first turn is fired. Its SHA-256 is stamped into every hourly snapshot;
29
+ editing it mid-run turns the run `invalid` instead of producing a verdict.
30
+ 2. **Preflight.** `insika soak --preflight` refuses to start unless the target
31
+ answers `/v1/vitals` with a readable RSS, turns carry the `INSIKA_TURN_TIMING`
32
+ breakdown *and* a `usage` block, the pid is stable, and the target host
33
+ matches the envelope. Each check is a named refusal, not a runtime warning.
34
+ 3. **Run.** `insika soak --run` fires the arrival process (Poisson, seeded,
35
+ concurrency-capped), polls vitals hourly, and appends every observation to
36
+ `<out>/<run>.jsonl` as it happens — a runner that dies at hour 60 must
37
+ leave 60 usable hours behind. No retry, no self-healing: a failed turn is
38
+ evidence.
39
+ 4. **Read the verdict.** `insika soak --verify <file>.jsonl.gz` recomputes the
40
+ whole verdict offline from the raw records — no network, no target, no
41
+ clock. Two people running it get the same answer; so does CI.
42
+
43
+ ## What is gated, and what is only reported
44
+
45
+ The gate is on the part **Insika owns**:
46
+
47
+ | Gated (binding) | Reported (never gated) |
48
+ |-----------------|------------------------|
49
+ | `rss_growth_ratio` — upper 95% bound of the RSS fit, hour 6 → hour 72 | `rss_slope_mb_per_day`, `rss_peak_mb` |
50
+ | `prep_p95_drift_ratio` — p95 of the local per-turn work (`prep_ms`: context build, policy, guardrails, chat assembly), last 6 h vs first measured 6 h | `ttft_p95_ms`, `total_p95` hourly series |
51
+ | `restarts_max` — any `boot_id` **or** `pid` change is a fail; there is no "declared deploy" exception | `heap_growth_ratio`, `db_growth_mb` |
52
+ | `error_rate_ceiling`, `no_usage_rate_ceiling` | `cost_usd`, `tokens_per_turn` |
53
+ | calibrated absolute ceilings (written from the E1 dry run, ×2) | — |
54
+
55
+ Time-to-first-token is **reported, never gated**: it was measured at ~720 ms and
56
+ shown to be ~100% provider. Gating on a number the engine cannot move would mean
57
+ failing a clean soak because the provider had a bad afternoon — and then
58
+ loosening the envelope, which the protocol forbids.
59
+
60
+ The **`no_usage_rate`** ceiling is the trap detector. The edge limiter answers a
61
+ breach with a canned reply, zero LLM calls and HTTP 200 — so a soak that ran
62
+ entirely into the limiter looks like thousands of successful turns with a
63
+ gorgeous 2 ms p95 and flat memory. A turn with no `usage` block called no model;
64
+ the ceiling keeps that from reading as a pass.
65
+
66
+ ## The isolation contract
67
+
68
+ - **Staging or a dedicated tenant, never a production tenant.** The run writes
69
+ ~4,000 sessions and tasks into the store; against production that is
70
+ synthetic data mixed into real customer history. The envelope names the
71
+ tenant (`soak`), so `delete_tenant_data` cleans the entire run in one
72
+ command.
73
+ - **Its own agent, with the edge limits off.** At a real ~48k tokens per turn,
74
+ the production token ceiling admits about 10 turns per hour — a 60 turns/h
75
+ soak against a production-configured agent measures its own limiter within
76
+ minutes. `chat_rate_limit: 0` and `agent_token_ceiling: 0` on the soak agent
77
+ are part of the frozen envelope, not an afternoon decision.
78
+ - **Deploys are frozen for the window.** Any restart is a fail, including a
79
+ deployment — a marker the operator writes after the fact would turn every
80
+ crash into a deploy.
81
+
82
+ ## Running it
83
+
84
+ ```bash
85
+ # the plan + one sample request, no traffic
86
+ insika soak --dry-run --envelope soak-envelope.md
87
+
88
+ # every precondition, and nothing else
89
+ insika soak --preflight --envelope soak-envelope.md
90
+
91
+ # the run itself (INSIKA_URL + OPENCLAW_GATEWAY_TOKEN, like loadtest.rb)
92
+ INSIKA_URL=https://<target> insika soak --run --envelope soak-envelope.md --out soak-out/
93
+
94
+ # resume after a short outage (the gap is recorded and counts against the window)
95
+ insika soak --run --resume soak-out/staging-2026-08-20T09-00-00Z.jsonl
96
+
97
+ # the verdict, recomputed offline — the only step that needs no target
98
+ insika soak --verify soak-out/staging-2026-08-20T09-00-00Z.jsonl.gz
99
+ ```
100
+
101
+ Duration, rate and ceilings all come from the envelope — there is no
102
+ `--duration` override, so a short run cannot be passed off as a soak.
103
+
104
+ ## Reading the verdict
105
+
106
+ `insika soak --verify` prints one of four, in this order of precedence:
107
+
108
+ - **`invalid`** — the snapshots carry more than one envelope hash, more than
109
+ one process id appeared under one boot generation, or the file is materially
110
+ truncated. **No metric is reported at all**; the file cannot be argued with.
111
+ - **`insufficient`** — coverage is short: a gap over 15 minutes, an hour under
112
+ the turn floor, or a runner that did not reach hour 72. Short coverage
113
+ restarts the window; it is never rounded up into a pass.
114
+ - **`fail`** — a gated ceiling was breached. The report names each one and
115
+ prints the leak-hunt starting point: whether the Ruby heap grew with RSS
116
+ (Ruby-side retention) or stayed flat while RSS climbed (allocator or a
117
+ native buffer).
118
+ - **`pass`** — none of the above.
119
+
120
+ A fail means *find the leak* — never cut, and never loosen the envelope.
121
+
122
+ ## See also
123
+
124
+ - [Observability](OBSERVABILITY.md) — `GET /v1/vitals`, the process readings
125
+ the soak samples hourly.
126
+ - [Deploy](DEPLOY.md) — the process model; `WEB_CONCURRENCY=1` is a soak
127
+ precondition, not only a queue-semantics one.
data/docs/TOOLS.md CHANGED
@@ -91,8 +91,10 @@ retries against. Structure is strict; a scalar may arrive in its lossless string
91
91
 
92
92
  - `{{param}}` — a declared top-level parameter, filled from the model's call.
93
93
  - `{{ctx.*}}` — turn context set **server-side, never by the model**: a closed set
94
- of `chat_id`, `store_id`, `agent_id`, `tenant`. This is how a tool knows *which*
95
- session/agent it is acting for without trusting the model.
94
+ of `chat_id`, `store_id`, `agent_id`, `tenant`, `image_url`. This is how a tool knows *which*
95
+ session/agent it is acting for without trusting the model. `image_url` is the
96
+ first image part on the message (a photo for analysis outside the prompt);
97
+ absent when the turn carried none.
96
98
  - `{{secret.*}}` — allowed **only** inside a header named in `secret_headers`.
97
99
  A secret placeholder anywhere else is rejected (it would leak unmasked). The
98
100
  real secret value is injected at provision time and never lives on disk.
@@ -181,6 +183,72 @@ backend, not of whoever calls it. Every agent sharing the tool gets the same val
181
183
  > there **preserves** it — the form carries the stored values through instead of
182
184
  > replacing the record with only what it shows.
183
185
 
186
+ ## Evidence: the lean envelope and grounding (RFC-0029)
187
+
188
+ A catalog tool returns products; the model should only ever quote the ones the tool
189
+ actually returned — the store dies of a SKU the model invented. `evidence` is the
190
+ declaration that makes "no claim without a tool ID" an engine rule instead of a
191
+ prompt convention. One declaration does **both** jobs: the engine strips the result
192
+ down to what the model sees (the lean envelope) **and** records every returned id on
193
+ the session's evidence ledger. There is no "lean but not evidence" mode.
194
+
195
+ ```jsonc
196
+ { "name": "search_products",
197
+ "response": { "extract": "evidence_envelope" },
198
+ "evidence": "products" } // bare kind
199
+
200
+ { "evidence": { "kind": "products", // full form
201
+ "items": "results", // non-default paths
202
+ "attachments": "cards" } }
203
+ ```
204
+
205
+ - `evidence_envelope` is the canonical extract: the raw response body arrives under
206
+ an engine-only key, the envelope parses `items`/`attachments` out of it, and
207
+ **nothing re-fattens** — the transcript and the tool trace record only the lean
208
+ result. It **requires** the `evidence` declaration (refused at load otherwise).
209
+ - **Wire contract** — the lean result the model sees is always
210
+ `{ "items": [ { "id": "…", "line": "…" } ] }` (≤ 16 items; `line` truncated to
211
+ 200 chars). A tool whose result has no valid items yields `{ "items": [] }`,
212
+ never a null. A malformed evidence result becomes `{ "error": … }` back to the
213
+ model — a correctable tool answer, exactly like a malformed call.
214
+ - **Attachments** are the optional second half: `[{ "type": "card"|"image",
215
+ "url": "…", "caption": "…" }]` (≤ 16, url ≤ 500 chars, malformed dropped). They
216
+ **never** reach the model context or the transcript — they ride the channel
217
+ delivery as an additive `attachments` key on the outbox payload, and the channel
218
+ (or its consumer) decides what a card looks like.
219
+ - A **code tool** opts in the same way: it either returns `{ items, attachments }`
220
+ directly and declares `evidence` in its registry metadata, or exposes an
221
+ `evidence` reader. No declaration = today's tool behavior, byte for byte.
222
+
223
+ ### Grounding: policing claims against the ledger
224
+
225
+ With the ledger fed, the pack declares how claims are policed — data on the agent,
226
+ not a separate code path (see [Agents](AGENTS.md)):
227
+
228
+ ```ruby
229
+ grounding mode: :flag, matcher: { sku: '\b[A-Z]{2,4}\d{4,8}\b' }
230
+ ```
231
+
232
+ - `mode` is `flag` (the default — audit), `enforce` (cut), or `off`. Absent = off.
233
+ - `matcher.sku` is a regex for the store's SKU shape, applied to the final answer;
234
+ every match that is **not** in the evidence ledger is an ungrounded claim.
235
+ Grounding is **SKU-only** by design: a name-based half cannot flag anything
236
+ without a "this is a product name" signal, so the ledger grounds ids, and the
237
+ model quoting a returned product by its *name* is simply outside the check
238
+ (the SKU path is the claim detector). A `sku` that does not compile is refused
239
+ at build; a matcher with no `sku` builds but matches nothing — `insika doctor`
240
+ warns about it.
241
+ - **`flag`** appends an `:ungrounded` flag (category `ungrounded`, source
242
+ `evidence`) to the existing `:guardrail_flagged` event — audit after the fact,
243
+ like every other output flag.
244
+ - **`enforce`** *cuts the sentence* containing an ungrounded claim from the content
245
+ the turn persists and delivers, and the flag carries `action: "cut"` so the audit
246
+ can tell a cut from a flag. It is honest about streaming: on a streaming surface
247
+ the already-streamed bytes are the channel's reality, which is exactly why the
248
+ default is `flag` — ship `enforce` only after a matcher audit proves precision.
249
+ - Grounding is **independent of the guardrails opt-in**: an agent with guardrails
250
+ off and `grounding.mode: :flag` still gets the check.
251
+
184
252
  ## Registering a tool
185
253
 
186
254
  A tool appears in the Studio panel and enters an agent's tool-loop when it is
data/docs/WHY.md CHANGED
@@ -32,7 +32,7 @@ the same speed-to-first-agent story Python teams tell, without leaving Ruby:
32
32
  require "insika"
33
33
 
34
34
  agent = Insika.agent("assistant") do
35
- model "deepseek-chat"
35
+ model "deepseek-v4-flash"
36
36
  provider :deepseek
37
37
  instructions "You are a concise, friendly assistant."
38
38
  end
data/docs/WORKFLOWS.md CHANGED
@@ -40,8 +40,8 @@ agents by id and those agents must resolve in the same runtime:
40
40
  newsroom = Insika.system do
41
41
  provider :deepseek
42
42
 
43
- agent("writer") { model "deepseek-chat"; instructions "Write ONE paragraph." }
44
- agent("editor") { model "deepseek-chat"; instructions "Rewrite as ONE sentence." }
43
+ agent("writer") { model "deepseek-v4-flash"; instructions "Write ONE paragraph." }
44
+ agent("editor") { model "deepseek-v4-flash"; instructions "Rewrite as ONE sentence." }
45
45
 
46
46
  workflow "publish",
47
47
  description: "Draft a paragraph, then tighten it.",