insika 0.8.0 → 1.0.1

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 (276) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +312 -2
  3. data/README.md +10 -6
  4. data/bin/insika +4 -1
  5. data/bin/insika-router +0 -1
  6. data/docs/AGENTS.md +142 -18
  7. data/docs/API.md +73 -0
  8. data/docs/ARCHITECTURE.md +56 -50
  9. data/docs/ARTIFACTS.md +33 -3
  10. data/docs/BENCHMARK.md +50 -7
  11. data/docs/CHANNELS.md +19 -2
  12. data/docs/CONTEXT.md +96 -27
  13. data/docs/CROSS-HARNESS-BENCH.md +157 -0
  14. data/docs/DEPLOY.md +70 -13
  15. data/docs/EMBEDDING.md +10 -0
  16. data/docs/EVALS.md +129 -10
  17. data/docs/FACTS.md +12 -6
  18. data/docs/HARVEST.md +8 -3
  19. data/docs/KNOWLEDGE.md +14 -0
  20. data/docs/MEDIA.md +6 -2
  21. data/docs/OBSERVABILITY.md +164 -28
  22. data/docs/PERFORMANCE.md +110 -0
  23. data/docs/PLUGINS.md +4 -4
  24. data/docs/POLICY.md +26 -14
  25. data/docs/RELEASING.md +9 -0
  26. data/docs/ROUTER.md +7 -0
  27. data/docs/RUBYLLM_2_MIGRATION.md +307 -0
  28. data/docs/SECURITY.md +34 -1
  29. data/docs/SHARED-CONVERSATIONS.md +40 -0
  30. data/docs/SKILLS.md +5 -3
  31. data/docs/STUDIO.md +122 -0
  32. data/docs/TOOLS.md +238 -42
  33. data/docs/operate.md +3 -0
  34. data/docs/prompts/ADD-TOOL.md +12 -2
  35. data/docs/prompts/DIAGNOSE-TURN.md +3 -0
  36. data/docs/prompts/GO-LIVE.md +3 -1
  37. data/lib/insika/agent_defaults.rb +140 -0
  38. data/lib/insika/agent_file_store.rb +12 -0
  39. data/lib/insika/agent_profile.rb +99 -17
  40. data/lib/insika/artifact_signing.rb +11 -0
  41. data/lib/insika/channels/web/widget.js +33 -0
  42. data/lib/insika/channels/web.rb +5 -2
  43. data/lib/insika/chat_builder.rb +124 -28
  44. data/lib/insika/checkpoint.rb +6 -1
  45. data/lib/insika/checkpoint_store.rb +59 -7
  46. data/lib/insika/commands/agent_payload.rb +3 -3
  47. data/lib/insika/commands/delete_kit.rb +26 -0
  48. data/lib/insika/commands/gate_harvest.rb +3 -0
  49. data/lib/insika/commands/review_guardrail_flag.rb +38 -0
  50. data/lib/insika/commands/run_distillation.rb +5 -8
  51. data/lib/insika/commands/run_harvest.rb +17 -1
  52. data/lib/insika/commands/seed_session.rb +128 -0
  53. data/lib/insika/commands/send_message.rb +9 -3
  54. data/lib/insika/commands/update_settings.rb +23 -1
  55. data/lib/insika/commands/write_kit.rb +27 -0
  56. data/lib/insika/compaction.rb +33 -7
  57. data/lib/insika/config_store.rb +87 -7
  58. data/lib/insika/context/builder.rb +29 -9
  59. data/lib/insika/context/priority.rb +5 -0
  60. data/lib/insika/context/provider.rb +7 -2
  61. data/lib/insika/context/providers/briefing.rb +11 -8
  62. data/lib/insika/context/providers/fence_notice.rb +27 -0
  63. data/lib/insika/context/providers/knowledge.rb +32 -6
  64. data/lib/insika/context/providers/memory.rb +61 -8
  65. data/lib/insika/context/providers/pending_confirmation.rb +52 -0
  66. data/lib/insika/context/providers/prompt.rb +17 -4
  67. data/lib/insika/context/providers/session.rb +46 -6
  68. data/lib/insika/context/reranker.rb +75 -0
  69. data/lib/insika/distill.rb +22 -13
  70. data/lib/insika/doctor.rb +142 -4
  71. data/lib/insika/dsl/runtime.rb +37 -4
  72. data/lib/insika/dsl/server_boot.rb +4 -12
  73. data/lib/insika/dsl.rb +19 -5
  74. data/lib/insika/edge_limiter.rb +27 -16
  75. data/lib/insika/egress_guard.rb +11 -3
  76. data/lib/insika/env_schema.rb +33 -73
  77. data/lib/insika/errors.rb +1 -0
  78. data/lib/insika/evals/assertions.rb +259 -18
  79. data/lib/insika/evals/golden.rb +230 -4
  80. data/lib/insika/evals/judge.rb +19 -10
  81. data/lib/insika/evals/moderator_compare.rb +47 -0
  82. data/lib/insika/evals/report.rb +61 -9
  83. data/lib/insika/evals/runner.rb +64 -4
  84. data/lib/insika/evals/simulator.rb +21 -3
  85. data/lib/insika/evals/transport.rb +317 -15
  86. data/lib/insika/event_stream.rb +63 -7
  87. data/lib/insika/evidence.rb +105 -17
  88. data/lib/insika/executor.rb +698 -164
  89. data/lib/insika/fence.rb +96 -0
  90. data/lib/insika/golden_store.rb +3 -0
  91. data/lib/insika/harvest.rb +26 -14
  92. data/lib/insika/harvest_engine.rb +1 -0
  93. data/lib/insika/harvest_store.rb +12 -0
  94. data/lib/insika/http_client.rb +87 -4
  95. data/lib/insika/kits.rb +54 -0
  96. data/lib/insika/knowledge.rb +34 -20
  97. data/lib/insika/llm_configurator.rb +46 -4
  98. data/lib/insika/llm_provider_store.rb +1 -3
  99. data/lib/insika/llm_trace_store.rb +54 -0
  100. data/lib/insika/mcp_client.rb +27 -63
  101. data/lib/insika/mcp_live_tool.rb +55 -9
  102. data/lib/insika/mcp_store.rb +37 -3
  103. data/lib/insika/mcp_tool_registry.rb +76 -26
  104. data/lib/insika/media.rb +38 -25
  105. data/lib/insika/memory_store.rb +16 -3
  106. data/lib/insika/memory_trim.rb +60 -0
  107. data/lib/insika/middleware.rb +12 -3
  108. data/lib/insika/model_metrics_store.rb +285 -0
  109. data/lib/insika/model_registry.rb +110 -0
  110. data/lib/insika/model_resolver.rb +1 -1
  111. data/lib/insika/model_selection.rb +14 -21
  112. data/lib/insika/model_visible.rb +17 -6
  113. data/lib/insika/onboarding.rb +1 -0
  114. data/lib/insika/outcome_store.rb +1 -1
  115. data/lib/insika/overlay_tool_registry.rb +30 -2
  116. data/lib/insika/pack_importer.rb +54 -9
  117. data/lib/insika/packaging.rb +14 -5
  118. data/lib/insika/pending_action_store.rb +45 -18
  119. data/lib/insika/plugin/loader.rb +4 -2
  120. data/lib/insika/prefix_fingerprint.rb +32 -27
  121. data/lib/insika/profile_source.rb +5 -0
  122. data/lib/insika/provider_error_classifier.rb +18 -3
  123. data/lib/insika/queue_policy.rb +7 -2
  124. data/lib/insika/refinement/proposer.rb +42 -17
  125. data/lib/insika/reliability.rb +12 -3
  126. data/lib/insika/router/app.rb +14 -5
  127. data/lib/insika/router/session_key.rb +35 -5
  128. data/lib/insika/safety/config.rb +20 -3
  129. data/lib/insika/safety/corpus.rb +10 -3
  130. data/lib/insika/safety/factory.rb +39 -1
  131. data/lib/insika/safety/input_guardrail.rb +21 -1
  132. data/lib/insika/safety/judge_moderator.rb +70 -0
  133. data/lib/insika/safety/output_validator.rb +29 -5
  134. data/lib/insika/safety/prompt_echo.rb +89 -0
  135. data/lib/insika/schema_guard.rb +53 -76
  136. data/lib/insika/server/app.rb +58 -7
  137. data/lib/insika/server/rack_app.rb +2 -0
  138. data/lib/insika/server/responses.rb +50 -5
  139. data/lib/insika/session_actor.rb +58 -28
  140. data/lib/insika/session_store.rb +163 -11
  141. data/lib/insika/settings_store.rb +57 -2
  142. data/lib/insika/shared_conversations.rb +305 -0
  143. data/lib/insika/skill_catalog.rb +28 -2
  144. data/lib/insika/skill_store.rb +6 -0
  145. data/lib/insika/soak/runner.rb +1 -1
  146. data/lib/insika/spoken_transcript.rb +31 -0
  147. data/lib/insika/steer_injector.rb +7 -48
  148. data/lib/insika/store.rb +29 -1
  149. data/lib/insika/stores/json_value.rb +37 -0
  150. data/lib/insika/stores/memory.rb +22 -35
  151. data/lib/insika/stores/postgres.rb +295 -0
  152. data/lib/insika/stores/sqlite.rb +257 -43
  153. data/lib/insika/stores/turn_counter.rb +25 -0
  154. data/lib/insika/studio/app.rb +1459 -163
  155. data/lib/insika/studio/assets/dist/application.css +1 -1
  156. data/lib/insika/studio/assets/dist/application.js +25 -25
  157. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  158. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  159. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  160. data/lib/insika/studio/assets/dist/fonts/ibm-plex-sans-latin-wght-normal.woff2 +0 -0
  161. data/lib/insika/studio/forms.rb +189 -12
  162. data/lib/insika/studio/nav_icons.rb +3 -0
  163. data/lib/insika/studio/views/_agent_config_retrieval.erb +73 -0
  164. data/lib/insika/studio/views/_agent_picker.erb +15 -0
  165. data/lib/insika/studio/views/_agent_tab_config.erb +83 -243
  166. data/lib/insika/studio/views/_agent_tab_history.erb +56 -1
  167. data/lib/insika/studio/views/_agent_tab_performance.erb +34 -0
  168. data/lib/insika/studio/views/_agent_tab_prompts.erb +7 -9
  169. data/lib/insika/studio/views/_agent_tab_skills.erb +16 -7
  170. data/lib/insika/studio/views/_agent_tab_tools.erb +90 -0
  171. data/lib/insika/studio/views/_agents_master.erb +8 -12
  172. data/lib/insika/studio/views/_cfg_alerts.erb +4 -0
  173. data/lib/insika/studio/views/_cfg_budget.erb +20 -0
  174. data/lib/insika/studio/views/_cfg_distill.erb +26 -0
  175. data/lib/insika/studio/views/_cfg_followup.erb +30 -0
  176. data/lib/insika/studio/views/_cfg_grounding.erb +21 -0
  177. data/lib/insika/studio/views/_cfg_guardrails.erb +47 -0
  178. data/lib/insika/studio/views/_cfg_harvest.erb +35 -0
  179. data/lib/insika/studio/views/_cfg_reliability.erb +32 -0
  180. data/lib/insika/studio/views/_chats_master.erb +66 -0
  181. data/lib/insika/studio/views/_customers_master.erb +37 -0
  182. data/lib/insika/studio/views/_empty.erb +15 -0
  183. data/lib/insika/studio/views/_facts_master.erb +42 -0
  184. data/lib/insika/studio/views/_harvest_candidate_detail.erb +14 -0
  185. data/lib/insika/studio/views/_identity.erb +16 -0
  186. data/lib/insika/studio/views/_kpi.erb +17 -0
  187. data/lib/insika/studio/views/_message.erb +40 -2
  188. data/lib/insika/studio/views/_model_chart.erb +82 -0
  189. data/lib/insika/studio/views/_refinement_master.erb +38 -0
  190. data/lib/insika/studio/views/{tool_edit.erb → _tool_edit.erb} +38 -11
  191. data/lib/insika/studio/views/_tool_users.erb +17 -0
  192. data/lib/insika/studio/views/_why.erb +53 -0
  193. data/lib/insika/studio/views/agent_detail.erb +17 -10
  194. data/lib/insika/studio/views/agents.erb +28 -16
  195. data/lib/insika/studio/views/approvals.erb +16 -11
  196. data/lib/insika/studio/views/artifact.erb +20 -7
  197. data/lib/insika/studio/views/artifacts.erb +19 -14
  198. data/lib/insika/studio/views/chats.erb +22 -29
  199. data/lib/insika/studio/views/customer.erb +88 -43
  200. data/lib/insika/studio/views/customers.erb +16 -26
  201. data/lib/insika/studio/views/evals.erb +49 -31
  202. data/lib/insika/studio/views/fact_detail.erb +114 -0
  203. data/lib/insika/studio/views/facts.erb +15 -116
  204. data/lib/insika/studio/views/followups.erb +63 -52
  205. data/lib/insika/studio/views/funnel.erb +104 -43
  206. data/lib/insika/studio/views/harvest.erb +85 -59
  207. data/lib/insika/studio/views/home.erb +138 -23
  208. data/lib/insika/studio/views/inbox.erb +36 -0
  209. data/lib/insika/studio/views/kits.erb +176 -0
  210. data/lib/insika/studio/views/knowledge.erb +92 -80
  211. data/lib/insika/studio/views/layout.erb +42 -11
  212. data/lib/insika/studio/views/live.erb +45 -0
  213. data/lib/insika/studio/views/mcp.erb +43 -13
  214. data/lib/insika/studio/views/models.erb +187 -0
  215. data/lib/insika/studio/views/parity.erb +38 -32
  216. data/lib/insika/studio/views/playground.erb +13 -9
  217. data/lib/insika/studio/views/refinement.erb +15 -224
  218. data/lib/insika/studio/views/refinement_detail.erb +243 -0
  219. data/lib/insika/studio/views/session.erb +153 -118
  220. data/lib/insika/studio/views/settings.erb +86 -22
  221. data/lib/insika/studio/views/skills.erb +53 -19
  222. data/lib/insika/studio/views/system_files.erb +7 -7
  223. data/lib/insika/studio/views/task.erb +45 -15
  224. data/lib/insika/studio/views/tasks.erb +35 -18
  225. data/lib/insika/studio/views/tools.erb +62 -73
  226. data/lib/insika/task_actor.rb +4 -1
  227. data/lib/insika/task_store.rb +67 -1
  228. data/lib/insika/telemetry/pricing.rb +16 -14
  229. data/lib/insika/telemetry/recorder.rb +115 -9
  230. data/lib/insika/telemetry/ruby_llm_instrumenter.rb +110 -0
  231. data/lib/insika/telemetry.rb +12 -2
  232. data/lib/insika/templates/daily-digest/README.md +13 -0
  233. data/lib/insika/templates/daily-digest/agent.rb +22 -11
  234. data/lib/insika/testing/store_contract.rb +65 -0
  235. data/lib/insika/text_tool_call.rb +66 -0
  236. data/lib/insika/tick.rb +7 -1
  237. data/lib/insika/token_store.rb +5 -0
  238. data/lib/insika/tool_assembly.rb +21 -13
  239. data/lib/insika/tool_batch.rb +1 -1
  240. data/lib/insika/tool_catalog.rb +12 -2
  241. data/lib/insika/tool_definition.rb +82 -13
  242. data/lib/insika/tool_envelope.rb +266 -29
  243. data/lib/insika/tool_manifest.rb +1 -0
  244. data/lib/insika/tool_store.rb +12 -4
  245. data/lib/insika/tool_trace_store.rb +15 -8
  246. data/lib/insika/tool_usage_report.rb +14 -4
  247. data/lib/insika/tools/a2a_remote.rb +1 -1
  248. data/lib/insika/tools/agent_enum.rb +1 -1
  249. data/lib/insika/tools/confirm_pending.rb +112 -0
  250. data/lib/insika/tools/data_defined_tool.rb +24 -15
  251. data/lib/insika/tools/generate_image.rb +2 -2
  252. data/lib/insika/tools/load_knowledge.rb +7 -3
  253. data/lib/insika/tools/load_skill.rb +3 -3
  254. data/lib/insika/tools/present.rb +115 -0
  255. data/lib/insika/tools/remember.rb +7 -2
  256. data/lib/insika/tools/run_persona_eval.rb +9 -6
  257. data/lib/insika/tools/save_artifact.rb +16 -6
  258. data/lib/insika/tools/schedule_followup.rb +3 -3
  259. data/lib/insika/tools/stuck_signal.rb +3 -3
  260. data/lib/insika/tools/subagent.rb +5 -5
  261. data/lib/insika/tools/subagents.rb +3 -3
  262. data/lib/insika/tools/tool_search.rb +11 -7
  263. data/lib/insika/tools/tts.rb +2 -2
  264. data/lib/insika/tools/update_briefing.rb +4 -4
  265. data/lib/insika/turn_output.rb +2 -2
  266. data/lib/insika/turn_state.rb +31 -1
  267. data/lib/insika/turn_timing.rb +51 -14
  268. data/lib/insika/version.rb +1 -1
  269. data/lib/insika/wiring/graph.rb +88 -16
  270. data/lib/insika/wiring/graph_chat.rb +4 -0
  271. data/lib/insika/workflow.rb +25 -64
  272. data/lib/insika.rb +23 -3
  273. metadata +74 -13
  274. data/lib/insika/commands/import_mcp_tools.rb +0 -48
  275. data/lib/insika/mcp_http_client.rb +0 -67
  276. data/lib/insika/mcp_tool_ingestor.rb +0 -165
data/docs/SKILLS.md CHANGED
@@ -249,9 +249,11 @@ in the allowlist → the model never sees it in `<available_skills>`.
249
249
  Two ways to satisfy both:
250
250
 
251
251
  - **Via a definition/pack import.** The import writes each skill directory into
252
- the store and sets the agent's `skills` allowlist **authoritatively** from the
253
- skills present — so a re-import that drops a skill also removes it. Keep the
254
- definition complete.
252
+ that agent's scope, without overwriting another agent's or a shared skill, and
253
+ sets the agent's `skills` allowlist **authoritatively** from the skills present.
254
+ Dropping a skill removes it from the allowlist, not storage. Keep the definition
255
+ complete. Packs previously imported into shared scope must be reimported from
256
+ their original sources; the overwritten shared body cannot recover each agent's version.
255
257
  - **Directly (Studio / API / DSL).** Write the skill (upserts the row and reloads
256
258
  the catalog atomically — live immediately), then attach it to the agent(s) by
257
259
  adding its name to the `skills` allowlist.
data/docs/STUDIO.md ADDED
@@ -0,0 +1,122 @@
1
+ ---
2
+ title: Studio
3
+ parent: Operate
4
+ nav_order: 0
5
+ permalink: /studio/
6
+ ---
7
+
8
+ # Studio — the operator UI
9
+
10
+ The Studio is the web UI at `/studio`. Sign in with `ADMIN_TOKEN` (see
11
+ [Deploy](DEPLOY.md#environment-variables)). This page covers the screens for
12
+ watching conversations, reviewing what waits on a person, and seeing what a
13
+ change did. Model metrics are on [Observability](OBSERVABILITY.md#studio-models).
14
+
15
+ Outside production, a banner above every page names the environment
16
+ (`INSIKA_ENV`, else `RACK_ENV`, else `local`): amber for staging, blue for
17
+ anything else. Production (`production` or `prod`) shows no banner.
18
+
19
+ ## Navigation
20
+
21
+ The sidebar groups pages as Operate, Review, Build, Insights and Platform. Each
22
+ Review page shows a badge with how many items wait on it. `⌘K` (or Search in the
23
+ sidebar) opens a palette that jumps to any page or agent.
24
+
25
+ ## Overview
26
+
27
+ `/studio/home`. The Overview shows the headline numbers for the agent filter and
28
+ a **Needs attention** row. The row lists the review queues, customers waiting for
29
+ a reply and tools failing in the last 24 hours, each linking to its page. Only
30
+ non-zero items show.
31
+
32
+ ## Live
33
+
34
+ `/studio/live` shows one card per recent conversation: customer, agent, the last
35
+ message, a presence dot (activity in the last 5 minutes) and the guardrail flag.
36
+ Cards move with the event stream. While a turn runs, a card shows the tools it
37
+ calls and "agent is writing…". A turn paused for an approval stops writing until
38
+ it resumes. A ticker lists the latest events, without the streaming deltas.
39
+
40
+ `?agent=<id>` shows only that agent's conversations.
41
+
42
+ ## Chats
43
+
44
+ `/studio/chats` lists conversations with three tabs: **All**, **Active now**
45
+ (activity in the last 5 minutes) and **Guardrail flagged**, each with its count.
46
+ The session page shows the customer (key, name, phone) and links to their other
47
+ conversations.
48
+
49
+ ### Why this reply
50
+
51
+ Each agent reply has a collapsed **why this reply** panel for its own turn:
52
+
53
+ - context tokens by category, and the skills injected with the reason;
54
+ - the tool calls with status and timing;
55
+ - the guardrail flags on the customer's message.
56
+
57
+ From the panel, **Replay in Playground** opens the conversation in the
58
+ Playground, and **Save as eval
59
+ case** opens the Evals editor prefilled from the reply (the customer's turns,
60
+ the transcript as the reference, the tools the turn called). A reply that
61
+ streams in live offers the panel when its turn finishes.
62
+
63
+ ### Tool calls
64
+
65
+ A turn with two or more tool calls draws a waterfall of when each one ran.
66
+ Tool calls are grouped by turn within each task, since the turn number restarts
67
+ at 1 for every task.
68
+
69
+ ### Markup tags
70
+
71
+ Tags an agent or a host writes in the text, like `<carrossel/>`,
72
+ `<vitrine formato="lista">` or `</b>`, show as highlighted tokens: in replies,
73
+ customer messages, and the Live and Chats previews. They are escaped and never
74
+ rendered. The channel decides what a tag does; the Studio only shows it is
75
+ there. A tag inside inline code stays code.
76
+
77
+ ## Guardrail false positives
78
+
79
+ On a guardrail flag in the session page, **Mark false positive** records the
80
+ verdict on the flag itself, inside the session, so retention and tenant purges
81
+ cover it. The flag can later be marked resolved, or the verdict undone.
82
+ Reviewing a flag does not count as conversation activity: the session keeps its
83
+ place in the recent lists. The command is `:review_guardrail_flag`.
84
+
85
+ ## Inbox
86
+
87
+ `/studio/inbox` is one list, oldest first, of everything that waits on a person:
88
+ approvals, fact proposals, mined skills, refinement runs and reported guardrail
89
+ false positives. Tabs filter by type, with counts. Each row opens the page where
90
+ the item is decided.
91
+
92
+ ## Agent History
93
+
94
+ The **History** tab of an agent shows outcomes per day over the last 30 days:
95
+ the funnel's primary outcome when the agent declares a funnel, else all
96
+ outcomes. A dashed line marks each change to a prompt file or to one of this
97
+ agent's own skills (shared skills change for every agent, so they are not
98
+ marked).
99
+
100
+ Under the chart, one card per change shows the file or skill, when, a short line
101
+ diff, and the outcomes in the 7 days after against the 7 days before. A window
102
+ that is not complete yet, or that starts before the outcome data, reads "not
103
+ enough data". A prompt card has the Restore form; a skill card links to the
104
+ skill.
105
+
106
+ ## Funnel: where conversations stop
107
+
108
+ The Funnel page marks the biggest loss between two consecutive declared stages.
109
+ Each loss opens (`?drop=<stage>`) the conversations that reached the stage but
110
+ not the next one in the period, grouped by the last thing that happened:
111
+
112
+ | Reason | Meaning |
113
+ |--------|---------|
114
+ | Last tool call failed | the conversation's last tool call returned an error |
115
+ | A guardrail flagged a message | a guardrail flagged one of its messages |
116
+ | Customer waiting on a reply | the customer wrote last |
117
+ | Customer went quiet after the agent's reply | the agent wrote last |
118
+ | No message to read | the conversation has no message to classify |
119
+ | Conversation no longer stored | the conversation was deleted |
120
+
121
+ Only the newest 200 stuck conversations are classified; the page says how many
122
+ were left out.
data/docs/TOOLS.md CHANGED
@@ -12,16 +12,16 @@ kinds, and the distinction that matters is **who can change one at runtime**:
12
12
 
13
13
  | | **Code tool** | **Data tool** | **MCP tool** |
14
14
  |---|---|---|---|
15
- | What | a Ruby class (`< RubyLLM::Tool`) | an HTTP call described by config, no Ruby | an MCP server's tool, called LIVE |
15
+ | What | a Ruby class (`< RubyLLM::Tool`) | an HTTP call or card presentation described by config | an MCP server's tool, called LIVE |
16
16
  | Lives | in the deployment image | as a row in SQLite | on the MCP server, behind a live client |
17
17
  | Editable at runtime | no (shipped in the image) | **yes** (DSL / API / manifest / Studio) | **yes** — enable/edit the *instance* (DSL / CLI / API / JSON import / Studio); the server owns its own tools |
18
- | Reach for it when | logic must run in-process (file edit, shell, subagent) | calling an external HTTP API | adopting a whole external MCP server's toolset |
18
+ | Reach for it when | logic must run in-process (file edit, shell, subagent) | calling an HTTP API or selecting evidence cards | adopting a whole external MCP server's toolset |
19
19
 
20
20
  **MCP tools are not data tools.** Configuring an enabled MCP **instance** (any
21
21
  surface below) is enough — its tools appear automatically, tagged
22
22
  `mcp:<instance>`, and each CALL goes straight to the server through a live,
23
- held client (stdio process / Streamable HTTP / SSE, with the full protocol
24
- handshake) — never a frozen snapshot. See [MCP servers](#mcp-servers) below.
23
+ held native RubyLLM client (stdio / Streamable HTTP, with discovery and legacy
24
+ handshake fallback) — never a frozen snapshot. See [MCP servers](#mcp-servers) below.
25
25
 
26
26
  Code tools **win name collisions** — you cannot register a data tool whose name
27
27
  shadows a code tool.
@@ -55,6 +55,9 @@ write path (`POST /v1/tools/manifest`) — writing this same shape via the DSL
55
55
  or Studio needs the literal header value instead; see
56
56
  "[The one gotcha](#the-one-gotcha-envsecret-templating-is-manifest-only)" below.
57
57
 
58
+ `{{ctx.chat_id}}` is the external session ID. In multi-tenant mode, it excludes
59
+ the authenticated tenant prefix used by Insika to isolate stored sessions.
60
+
58
61
  ### Parameters: the schema is the contract
59
62
 
60
63
  `parameters` is **JSON Schema**, and it reaches the provider verbatim — it is the only
@@ -120,7 +123,7 @@ once, at ingestion — see the gotcha below before reaching for it:
120
123
  (`object/array/string/number/integer/boolean`); `oneOf`/`anyOf`/`allOf`/`$ref`/
121
124
  `if`/`then`/`else` are forbidden (not every provider supports them).
122
125
  - `side_effect` defaults from the method (GET/HEAD → false, else true) and drives
123
- checkpoint/replay semantics (a completed side-effecting tool is not re-run on
126
+ serial execution within a session and checkpoint/replay semantics (a completed side-effecting tool is not re-run on
124
127
  resume — see [Architecture](ARCHITECTURE.md#durability-checkpoints-and-resume)).
125
128
 
126
129
  ### `halt_when`: when the answer is already out
@@ -199,12 +202,10 @@ backend, not of whoever calls it. Every agent sharing the tool gets the same val
199
202
 
200
203
  ## Evidence: the lean envelope and grounding
201
204
 
202
- A catalog tool returns products; the model should only ever quote the ones the tool
203
- actually returned — the store dies of a SKU the model invented. `evidence` is the
204
- declaration that makes "no claim without a tool ID" an engine rule instead of a
205
- prompt convention. One declaration does **both** jobs: the engine strips the result
206
- down to what the model sees (the lean envelope) **and** records every returned id on
207
- the session's evidence ledger. There is no "lean but not evidence" mode.
205
+ An `evidence` declaration reshapes a tool result into a lean list and records its
206
+ IDs in the session ledger. The ledger supplies the write gate, presentation tools
207
+ and optional output grounding below. Declaring evidence alone does not prevent
208
+ unsupported claims in the final answer.
208
209
 
209
210
  ```jsonc
210
211
  { "name": "search_products",
@@ -226,14 +227,98 @@ the session's evidence ledger. There is no "lean but not evidence" mode.
226
227
  never a null. A malformed evidence result becomes `{ "error": … }` back to the
227
228
  model — a correctable tool answer, exactly like a malformed call.
228
229
  - **Attachments** are the optional second half: `[{ "type": "card"|"image",
229
- "url": "…", "caption": "…" }]` (≤ 16, url ≤ 500 chars, malformed dropped). They
230
+ "url": "…", "caption": "…", "id": "…" }]` (≤ 16, url ≤ 500 chars, malformed dropped). They
230
231
  **never** reach the model context or the transcript — they ride the channel
231
232
  delivery as an additive `attachments` key on the outbox payload, and the channel
232
- (or its consumer) decides what a card looks like.
233
+ (or its consumer) decides what a card looks like. Supply an explicit `id` when
234
+ cards are not one-to-one with items in the same order. Without one, an attachment
235
+ takes its item's ID at the original position, before malformed cards are dropped.
236
+ - Lean `line` passes through the tool-result sanitizer only when `fencing` is on.
237
+ Attachment captions are normalized to UTF-8 but are not fenced. See
238
+ [Fencing](AGENTS.md#fencing--third-party-text-is-data-never-instructions).
233
239
  - A **code tool** opts in the same way: it either returns `{ items, attachments }`
234
240
  directly and declares `evidence` in its registry metadata, or exposes an
235
241
  `evidence` reader. No declaration = today's tool behavior, byte for byte.
236
242
 
243
+ ### Provenance: checking IDs before a write
244
+
245
+ Declare `requires_evidence` on a data-defined tool to accept only IDs previously
246
+ returned by an `evidence` tool in the same session:
247
+
248
+ ```json
249
+ { "requires_evidence": ["product_id"] }
250
+ ```
251
+
252
+ The full form is `{ "requires_evidence": { "params": ["product_id"] } }`.
253
+ The list must be non-empty and name declared top-level parameters. Scalar values
254
+ and every element of an array are converted to strings and compared exactly:
255
+ `SKU-1` and `sku-1` are different IDs. IDs typed by a customer do not count.
256
+ The ledger includes earlier turns and completed evidence results in the current
257
+ turn, capped at the latest 1,000 distinct IDs. Every declared parameter the call
258
+ carries must pass before any write occurs; a parameter the schema marks optional
259
+ and the model leaves out has nothing to check, a required one left out blocks.
260
+ Search first, then write in a later batch: a search and write in the same parallel
261
+ batch have no dependency ordering guarantee.
262
+
263
+ An unknown ID returns `status: "blocked"`, `gate: "provenance"`, the parameter,
264
+ the value, and an instruction to search or look it up before retrying. The backend
265
+ is never called and no operator approval is requested. A missing ledger blocks the
266
+ call too. Omit the declaration to keep the existing behavior; MCP tools do not
267
+ support this declaration.
268
+
269
+ The Studio tool editor exposes `requires_evidence`. Blocked calls appear in session
270
+ traces and `insika tools:report`, emit `tool_blocked` with name/gate/parameter only,
271
+ and increment `insika.tool.blocked`. `insika doctor` warns when an agent allows a
272
+ gated data tool without an allowed data tool declaring `evidence`.
273
+
274
+ ### Side effects in parallel batches
275
+
276
+ With `limits.tool_concurrency > 1`, tools marked `side_effect` execute one at a time
277
+ within the session's runtime. Unmarked tools still run concurrently, including
278
+ while a write is running. MCP tools are marked `side_effect: true` unless the server
279
+ annotates them `readOnlyHint`; a read-only `POST` data tool needs an explicit
280
+ `"side_effect": false` to keep its concurrency.
281
+ A queued write holds no concurrency slot. Different sessions remain independent;
282
+ backend rules such as quantity limits remain the backend's responsibility.
283
+
284
+ The per-tool timeout starts after both gates are acquired. Trace duration includes
285
+ queueing time, so it measures how long the model waited, not just backend execution.
286
+
287
+ ### Customer confirmation: a write the conversation approves
288
+
289
+ Some writes should not happen on the strength of one ambiguous line — closing an
290
+ order, deleting a record, sending a message on someone's behalf. Name them on the
291
+ agent and the engine holds the call for the customer's word:
292
+
293
+ ```ruby
294
+ customer_confirm "create_order" # DSL; the Studio field is "customer confirm"
295
+ ```
296
+
297
+ When the model calls a held tool, nothing runs. The engine records a pending action
298
+ for the session and returns a `pending_confirmation` result carrying the arguments
299
+ and an instruction: tell the customer exactly what will happen and ask. The turn ends
300
+ with that question. On the next message the open hold is rendered at the tail of the
301
+ context, and two system tools are available: `confirm_pending` runs the original call
302
+ with the arguments recorded at the hold (no path exists to confirm different ones),
303
+ `cancel_pending` drops it. A hold that the next message neither confirms nor cancels
304
+ expires on its own — a customer who changed the subject did not agree.
305
+
306
+ Three controls now sit on a write, and they compose in this order:
307
+
308
+ | Control | Who decides | What happens to the turn | Declared on |
309
+ | --- | --- | --- | --- |
310
+ | Provenance (`requires_evidence`) | the ledger | the call is refused, the model is told how to resolve the id | the tool |
311
+ | Customer confirmation (`customer_confirm`) | the customer, next message | the turn ends with a question | the agent |
312
+ | Operator approval (`approvals_required`) | a person in the Studio | the turn suspends and waits | the agent |
313
+
314
+ A tool is confirmed by the customer or approved by the operator, never both. The
315
+ confirmed run still passes provenance, fencing, the side-effect record and the trace;
316
+ the trace shows the hold as `gate: confirmation`, and the `:tool_result` event and the
317
+ Responses stream report it as `status: held`, which the evals treat as neither a
318
+ success nor an error. Whether the customer's "sim" means yes is the model's reading;
319
+ what the engine guarantees is that the write runs only after an explicit question, a
320
+ customer turn, and an explicit tool call.
321
+
237
322
  ### Grounding: policing claims against the ledger
238
323
 
239
324
  With the ledger fed, the pack declares how claims are policed — data on the agent,
@@ -263,20 +348,70 @@ grounding mode: :flag, matcher: { sku: '\b[A-Z]{2,4}\d{4,8}\b' }
263
348
  - Grounding is **independent of the guardrails opt-in**: an agent with guardrails
264
349
  off and `grounding.mode: :flag` still gets the check.
265
350
 
351
+ ## Presentation tools: the model picks ids, the engine shows the cards
352
+
353
+ A **presentation tool** selects which evidence cards to show. Declare it with
354
+ `presentation` instead of `request`; it runs in-process and is always
355
+ `side_effect: false`. The model supplies IDs, never card URLs or captions.
356
+
357
+ ```jsonc
358
+ { "name": "present_products",
359
+ "description": "Show product cards to the customer. Pass only ids a search returned.",
360
+ "parameters": [{ "name": "product_ids", "type": "array:string" },
361
+ { "name": "title", "type": "string", "required": false }],
362
+ "presentation": { "component": "product_cards", // what the channel renders
363
+ "ids": "product_ids", // the array:string parameter
364
+ "max": 8 } } // 1..16 (the attachment cap; default 16)
365
+ ```
366
+
367
+ Exactly one of `request` / `presentation`; `ids` must name a declared `array:string`
368
+ parameter; `component` follows the tool-name rule. Create it through the DSL,
369
+ manifest or API. The Studio form preserves an existing `presentation` declaration
370
+ on save but does not expose its fields. Requested IDs are deduplicated in order
371
+ and compared case-sensitively. The engine then:
372
+
373
+ 1. keeps only ids the session's **evidence ledger** has seen — the rest are dropped
374
+ with reason `unknown` (an id the model invented, or the customer typed);
375
+ 2. joins each kept id to the card an evidence tool returned **this session** — this
376
+ turn's first, then the last 64 the ledger kept (cards carry the `id` of the item
377
+ they stand for); a known id with no card is dropped with reason `no_card`;
378
+ 3. truncates to `max` — the overflow is dropped with reason `max`;
379
+ 4. records the selection on the turn and emits `:ui` on the stream (published as
380
+ `insika.ui` on `/v1/responses`, as the `ui` frame on the web channel);
381
+ 5. answers the model with `shown` (IDs) and `dropped` (objects with `id` and `reason`) — plus one
382
+ instruction when nothing could be shown ("name the products in text or search
383
+ again").
384
+
385
+ **Delivery.** When a turn made a presentation call, the outbox `attachments` are
386
+ *exactly the presented cards*, in call order, each stamped with the call's `component`
387
+ and `title`. A turn with no presentation call delivers every hoarded card, as before —
388
+ a pack that declares no presentation tool sees no change. An empty selection also
389
+ suppresses that fallback: it emits `count: 0` and delivers no cards from that call.
390
+ An earlier turn's ID can be shown without a fresh lookup while its card remains
391
+ in the session's 64-card ledger. IDs and cards have separate caps: a known ID can
392
+ still return `no_card`. Cards are stored snapshots, not a stock or price refresh.
393
+
394
+ A presentation tool needs an evidence source to populate the session's cards.
395
+ `insika doctor` warns when an agent allows presentation without an allowed evidence
396
+ data tool (`presentation-tools`); it cannot verify a code-tool evidence source. The line that tells the model
397
+ *when* to show cards ("show cards with present_products, ids only") is the pack's.
398
+
399
+ Not here: partial rendering while arguments stream, per-component enrichment (price
400
+ today, stock — the card is what the evidence tool returned), and a second component
401
+ such as suggestion chips (same mechanism, when a channel asks for it).
402
+
266
403
  ## Registering a tool
267
404
 
268
405
  A tool appears in the Studio panel and enters an agent's tool-loop when it is
269
406
  **registered** in the catalog **and** allowed by the agent's policy allowlist.
270
- Four ways to write a data tool into the store — all **hot** (registry and catalog
271
- reload, no restart):
407
+ Three ways to write a data tool into the store — all **hot** (registry and catalog
408
+ reload, no restart; with several workers, the others pick the change up within ~2 s):
272
409
 
273
410
  1. **DSL** — `data_tool(name:, …)` in a `Insika.agent { … }` block.
274
411
  2. **Studio** — the Tools panel editor.
275
412
  3. **Manifest** — `POST /v1/tools/manifest`. Partial failure is isolated: one
276
413
  malformed tool becomes an `errors[]` entry; only a structural manifest error
277
414
  fails the whole request. The response reports `{ version, created, updated, errors }`.
278
- 4. ~~MCP ingestion~~ — retired. An MCP server's tools are no
279
- longer written into this store at all; see [MCP servers](#mcp-servers).
280
415
 
281
416
  ### The one gotcha: env/secret templating is manifest-only
282
417
 
@@ -293,28 +428,67 @@ any other way must ship literal values** — a real URL, and a real (masked on
293
428
  read) header value. `{{ctx.*}}` and `{{param}}` work everywhere (they resolve
294
429
  at turn time, not ingestion).
295
430
 
431
+ The Studio **Tools** screen groups permissions by origin: native tools, HTTP
432
+ tools, and MCP tools by server instance. Selection remains per tool; grouping
433
+ does not change tool names or grant permissions.
434
+ Each section can be collapsed. Filtering by tool or server name opens matching
435
+ sections without changing selections; clearing the filter restores their state.
436
+ Section selection buttons affect every tool in that section, including filtered
437
+ tools, but never override denied tools. Save tools applies the changes.
438
+
439
+ An MCP instance is a connection, not a single tool. Enable its individual tools
440
+ for the agent in Studio. **Test connection** validates discovery with that
441
+ instance's credentials; it does not validate a separate HTTP data tool or every
442
+ remote tool's execution permissions. When replacing an HTTP tool with MCP,
443
+ remove the old tool from the agent's permissions and update prompt references
444
+ to the actual MCP tool names. Keep the egress allowlist restricted to required hosts.
445
+
296
446
  ## MCP servers
297
447
 
298
448
  An MCP **instance** is durable config — transport, target, credentials, an
299
449
  `enabled` flag — held in its own store, separate from data tools. Once an
300
450
  instance is enabled, its tools appear in the catalog automatically (group
301
- `mcp:<instance>`, `side_effect: true`), and every call goes straight to the
451
+ `mcp:<instance>`, `side_effect: true` unless annotated `readOnlyHint`), and every call goes straight to the
302
452
  server through a live, held client — the runtime never converts an MCP tool
303
453
  into a stored data tool.
304
454
 
305
- **Three transports**, picked by `transport:`:
455
+ Each tool is named after its instance: `<instance>__<tool>`, with the instance
456
+ lowercased and anything outside `a-z0-9` turned into `_` (instance
457
+ `shop-staging`, tool `list_products` → `shop_staging__list_products`). That is
458
+ the name the model calls and the one an agent's allowlist holds, so two
459
+ instances that offer the same tool (a store's production and staging) stay two
460
+ tools, each reaching its own server. Per-tool overrides (`tools:`) still use the
461
+ server's own tool name. A qualified name over 64 characters is skipped with a
462
+ warning, since providers reject it: shorten the instance name.
463
+
464
+ The deployment's legacy `import_mcp_tools` command now aliases
465
+ `refresh_mcp_tools`: it returns `{ instance:, tools: }` and emits
466
+ `mcp_tools_refreshed`, replacing the old created/updated import report.
467
+ Previously imported data tools remain unchanged and take precedence over live
468
+ tools with the same name. After refreshing and verifying the live instance,
469
+ delete those snapshots through the normal data-tool controls to use live calls.
470
+ `insika doctor` identifies them by their `mcp:<instance>` group.
471
+
472
+ **Two supported transports**, picked by `transport:`:
306
473
 
307
474
  | Transport | Target | Notes |
308
475
  |---|---|---|
309
476
  | `stdio` | `command` + `args`, run as a child process, `env` is its process environment | requires `INSIKA_MCP_STDIO=1` — see below |
310
- | `http` | `url` + `headers` (Streamable HTTP, the modern default) | egress-guarded like any outbound URL |
311
- | `sse` | `url` + `headers` | same egress guard as `http` |
477
+ | `http` | `url` + `headers` (Streamable HTTP) | egress-guarded; HTTPS required outside loopback |
478
+
479
+ Legacy `sse` records remain stored, but cannot connect through the native client.
480
+ Change the record to `http` with the server's Streamable HTTP endpoint. Event-stream
481
+ responses over Streamable HTTP remain supported; only the old SSE transport is removed.
312
482
 
313
483
  **The stdio gate.** A stdio instance is arbitrary command execution by
314
484
  config — it saves, but refuses to start ("stdio disabled by env") until the
315
485
  operator sets `INSIKA_MCP_STDIO=1` (config-over-convention, the same pattern
316
- as the egress envs). `http`/`sse` need no such gate; their URL is checked by
317
- the normal egress allowlist instead.
486
+ as the egress envs). `http` needs no such gate; its URL goes through the
487
+ egress guard instead: HTTPS, and a host that resolves to a private address
488
+ needs `INSIKA_EGRESS_ALLOW_PRIVATE` and a place in `INSIKA_EGRESS_HOSTS`. A
489
+ public host is not pinned by that list, since the operator fixed the URL and
490
+ the model cannot change it. Native MCP additionally refuses plain HTTP
491
+ outside loopback, even when `INSIKA_EGRESS_ALLOW_HTTP` is enabled.
318
492
 
319
493
  **Credentials are never visible in plaintext.** `env` (stdio) and `headers`
320
494
  (http/sse) mask every value as `__OCULTO__` on read, everywhere (CLI, API,
@@ -346,6 +520,31 @@ held client, which does its own discovery on first use regardless of whether
346
520
  `enabled` flag and its credentials are the **operator's** — a Studio/CLI/API
347
521
  edit made after boot is never clobbered back by the next restart.
348
522
 
523
+ The **first** boot after a declaration also lists the server's tools, because
524
+ until they are listed the agent is offered none of them and answers out of its
525
+ own head instead. A server that cannot be listed (stdio behind a closed gate, a
526
+ host that is down) does not stop the boot — it is warned about by name, and the
527
+ agent runs without those tools until `insika mcp refresh <name>` succeeds.
528
+
529
+ **What the deployment trusts each tool with** — a server describes what its tools
530
+ *do*; only the deployment can say which of its answers are evidence and which
531
+ parameter may only ever carry an id something already returned:
532
+
533
+ ```ruby
534
+ mcp "store", transport: :http, url: ENV.fetch("STORE_MCP"),
535
+ tools: {
536
+ # the store's own field names: no server renames them for us
537
+ "search_products" => { evidence: { kind: "products", items: "products",
538
+ id: "product_id", line: "line" } },
539
+ # a write that may only take an id the customer was actually shown
540
+ "add_to_cart" => { requires_evidence: ["product_id"] }
541
+ }
542
+ ```
543
+
544
+ Both are refused at ingestion, never at the turn: an `evidence:` typo that
545
+ survived would extract nothing and the gate below it would block every write.
546
+ A tool nobody names here behaves exactly as it did before.
547
+
349
548
  2. **CLI** — `insika mcp list | add | remove | import <file.json> | test <name> |
350
549
  refresh <name>`. `add` takes `--name`, `--transport`, `--command`/`--arg`
351
550
  (repeatable) or `--url`/`--header "Name: value"` (repeatable)/`--env
@@ -367,7 +566,7 @@ held client, which does its own discovery on first use regardless of whether
367
566
 
368
567
  `insika mcp import FILE.json` upserts every entry (a bare `command` implies
369
568
  `stdio`; a bare `url` implies `http`; add `"transport": "sse"` explicitly
370
- for SSE — the bare format has no other way to spell it). The same parser
569
+ only to retain a legacy SSE record pending migration). The same parser
371
570
  backs `PUT /v1/mcp` and Studio's "Import JSON" box; `export` produces the
372
571
  document back with secrets masked as `__OCULTO__`, so round-tripping an
373
572
  export never wipes a stored credential.
@@ -405,9 +604,10 @@ held client, which does its own discovery on first use regardless of whether
405
604
  A model can ask for several tools in one step. By default the engine runs them one
406
605
  at a time. Set `limits[:tool_concurrency]` above 1 (see
407
606
  [Agents](AGENTS.md#tool_concurrency--parallel-tool-calls)) and the calls in that
408
- batch run concurrently, **at most N in flight**, on the turn's own reactor — so
409
- the wall-clock of a batch of slow data tools approaches the slowest call rather
410
- than their sum. The cap covers every enveloped tool of the turn, including the
607
+ batch run concurrently, **at most N in flight**, on the turn's own reactor.
608
+ Tools marked `side_effect` still execute one at a time per session; queued writes
609
+ acquire that serial gate before taking a shared concurrency slot. See
610
+ [Side effects](#side-effects-in-parallel-batches). The cap covers every enveloped tool, including the
411
611
  ones `tool_search` promotes mid-turn.
412
612
 
413
613
  It applies only to what the *model* fans out. Two primitives already parallelize
@@ -434,7 +634,7 @@ Turning it on changes three things, all of them worth knowing before you do:
434
634
  Approvals and concurrency are mutually exclusive per turn — the approval gate wins
435
635
  and the turn goes serial. That is a deadlock avoided, not a preference.
436
636
 
437
- ## Egress: the SSRF guard (and its silent failure)
637
+ ## Egress: the SSRF guard
438
638
 
439
639
  Data tools make outbound HTTP, so every call passes through the **EgressGuard**, a
440
640
  Server-Side Request Forgery defense. The default posture is **strict: public
@@ -446,17 +646,10 @@ Server-Side Request Forgery defense. The default posture is **strict: public
446
646
  | `INSIKA_EGRESS_ALLOW_HTTP=1` | permit plain `http` — **loopback dev only** |
447
647
  | `INSIKA_EGRESS_ALLOW_PRIVATE=1` | permit private/loopback IPs — **dev only** |
448
648
 
449
- > ⚠️ **Egress failures are silent.** When a tool targets a blocked host (e.g. a
450
- > plain-`http` localhost backend without the opt-ins), the guard turns the block
451
- > into a `{ error: … }` returned **to the model** — the request never leaves the
452
- > process, yet the stream still emits a tool call, so the model narrates a
453
- > plausible failure and the conversation *looks* like it worked. You will not see
454
- > an exception.
455
- >
456
- > **Always verify by the trace, never by the reply:** open the Studio session
457
- > viewer — a healthy call shows the request, args, and the backend's `200`; a
458
- > missing or errored call is almost always egress (host not in the allowlist, or
459
- > `http`/private without the opt-in).
649
+ An egress rejection returns `{ error: … }` to the model without making the HTTP
650
+ request. Inspect the session trace for the actual error; a plausible reply does
651
+ not prove the backend ran. Provenance refusals instead report `status: "blocked"`
652
+ and `gate: "provenance"`.
460
653
 
461
654
  Egress is **orthogonal** to registration and allowlisting: a tool can be
462
655
  registered, allowed, offered to the model, and still blocked at call time.
@@ -471,8 +664,9 @@ Work down this checklist:
471
664
  `data-tools` check is the only place that says so.
472
665
  2. **Allowed for this agent?** In `tools_allow` (or an allowed group), and not in
473
666
  `tools_deny`?
474
- 3. **Egress?** If it *appears and is called* but "fails", open the trace — a
475
- blocked call is ~99% egress.
667
+ 3. **Call refused or failed?** Inspect the trace. For `gate: "provenance"`, look up
668
+ the ID through an evidence tool before retrying. For an error, check its message
669
+ for schema, egress, timeout or backend failures.
476
670
  4. **URL literal?** For non-manifest tools, an unresolved `{{env.*}}` would have
477
671
  422'd at import — re-check the definition.
478
672
 
@@ -490,7 +684,7 @@ routes, the signed link and the retention/LGPD reach.
490
684
  The per-session trace answers "what did this conversation call"; nothing used to
491
685
  answer "what does this agent carry and never use". The report aggregates the
492
686
  stored traces per agent (tasks → sessions → `tool_traces`, the same read the
493
- Studio does) and flags three shapes:
687
+ Studio does) and flags four shapes:
494
688
 
495
689
  - **`never_called`** — in `tools_allow`, zero calls in any stored trace. Dead
496
690
  weight: its schema ships on every request and buys nothing.
@@ -498,6 +692,8 @@ Studio does) and flags three shapes:
498
692
  the window (default 14 days). Either the tool is broken or the model cannot
499
693
  hold its contract.
500
694
  - **`stale`** — called at some point, but not once inside the window.
695
+ - **`blocked`** — gate refusals in the window, counted by gate. These do not count
696
+ as conventional tool errors.
501
697
 
502
698
  ```bash
503
699
  insika tools:report # every stored agent
data/docs/operate.md CHANGED
@@ -10,10 +10,13 @@ permalink: /operate/
10
10
  Running it once it is live: seeing what happened, firing turns nobody sent,
11
11
  handing back a report, and knowing what the box will take before it takes it.
12
12
 
13
+ - **[Studio](STUDIO.md)** — the operator UI: live conversations, why each reply, the review inbox, what a change did.
13
14
  - **[Observability](OBSERVABILITY.md)** — the event stream, and turns as OpenTelemetry traces and metrics.
14
15
  - **[Schedules](SCHEDULING.md)** — recurring turns the engine fires on its own tick.
15
16
  - **[Artifacts](ARTIFACTS.md)** — a report the agent can hand you a URL to.
16
17
  - **[Load test](LOADTEST.md)** — end-to-end load testing, and comparing topologies before you choose one.
17
18
  - **[Soak](SOAK.md)** — the long run that finds what a short one cannot.
18
19
  - **[Benchmark](BENCHMARK.md)** — the engine's measured overhead, reproducible without an API key.
20
+ - **[Cross-harness bench](CROSS-HARNESS-BENCH.md)** — the same store answered by six harnesses, graded on the store's own state.
21
+ - **[Performance](PERFORMANCE.md)** — what keeps a turn cheap, and what was tried and left out.
19
22
  {: .card-grid }
@@ -30,6 +30,7 @@ with one turn. Nothing more.
30
30
  | The need | The kind | Where it lives |
31
31
  |---|---|---|
32
32
  | Call an external HTTP API | **data tool** (`data_tool` in the DSL block) | a row in SQLite, editable at runtime |
33
+ | Show selected evidence cards | **presentation data tool** (`presentation`, no `request`) | the tool store; see [Tools](../TOOLS.md#presentation-tools-the-model-picks-ids-the-engine-shows-the-cards) |
33
34
  | Logic must run in-process | **code tool** (a Ruby class `< RubyLLM::Tool`) | the deployment image |
34
35
  | Adopt a whole external MCP server | **`mcp` instance** | durable config; its tools appear tagged `mcp:<name>` |
35
36
  | Teach a procedure (no data fetching) | **skill** (`skill "name", description:, instructions:`) | loads on demand via `load_skill` |
@@ -78,6 +79,12 @@ RULES:
78
79
  verbatim and arguments are checked against it at call time.
79
80
  - Author the FINAL url: the HTTP client does not follow redirects, and the egress guard
80
81
  cleared that host only.
82
+ - For a write that accepts catalog IDs, declare `requires_evidence` for those
83
+ parameters and ensure an allowed lookup declares `evidence`. Mark the write
84
+ `side_effect: true`; the engine serializes marked writes within a session.
85
+ - For presentation, follow the [card contract](../TOOLS.md#presentation-tools-the-model-picks-ids-the-engine-shows-the-cards):
86
+ pass known IDs to the presentation tool. Look up again if their cards are absent
87
+ from the session ledger or current information is needed.
81
88
  - Do not add a second capability "while we're here".
82
89
 
83
90
  ## Step 3 — Make sure it enters the tool-loop
@@ -96,8 +103,11 @@ instead of fighting it.
96
103
  ## Step 4 — Prove it with ONE turn
97
104
 
98
105
  Run one `reply()` whose message forces the call ("how many BRL is 1 USD right now?").
99
- The reply must use what the tool returned — if the model answers from imagination, the
100
- tool did not run: re-check Step 3 before touching the prompt.
106
+ Inspect the trace to confirm the call and result; the reply alone is not proof.
107
+ For a gated write, also verify that an unknown ID is blocked before any backend
108
+ request. For presentation, verify the selected `insika.ui` items. Use
109
+ [snapshot eval assertions](../EVALS.md#graders--what-a-turns-calls-and-reply-are-checked-against)
110
+ when the flow needs a repeatable precondition.
101
111
 
102
112
  ## Step 5 — Self-check
103
113
 
@@ -43,6 +43,9 @@ text), roughly **when**, and expected vs. actual. Reproduce once locally if chea
43
43
  | turn completed, customer got nothing | delivery is separate from the turn: check `channel_delivered` / `delivery_failed` | [Channels](../CHANNELS.md) |
44
44
  | freshly created agent returns empty turns | persona overflows the default `context_budget` (8000) | [Context](../CONTEXT.md) |
45
45
  | tool never called (or "missing") | not registered OR not allowed (`tools_allow`) | [Tools](../TOOLS.md) § troubleshooting |
46
+ | `tool_blocked` with `gate: provenance` | ID absent from session evidence; look up before retrying | [Tools](../TOOLS.md#provenance-checking-ids-before-a-write) |
47
+ | presentation shows no cards | inspect dropped IDs: `unknown`, `no_card`, `max`; a known ID may have no retained card | [API](../API.md#tool-and-presentation-sse-events) |
48
+ | cache reuse dropped | compare identity/tool-schema fingerprints with provider cache token accounting | [Context](../CONTEXT.md#the-provider-prefix-cache) |
46
49
  | identical `tool_call` repeated, then abort | the `max_tool_repeat` loop guard | [Agents](../AGENTS.md) § limits |
47
50
  | model gave up after one empty result | `tool_persistence` off (it is ON by default) | same |
48
51
  | `turn_stuck` event | the agent declared it cannot proceed — escalation signal, not a bug | [Agents](../AGENTS.md) § stuck |
@@ -87,7 +87,9 @@ In order, each with evidence:
87
87
 
88
88
  1. `curl https://<host>/up` → `{"status":"ok"}`.
89
89
  2. `bin/insika doctor` against the deployed volume (or via the platform's shell) —
90
- relay its findings verbatim; fix errors before continuing.
90
+ relay its findings verbatim; fix errors before continuing. Check evidence sources
91
+ for gated writes/presentation, review the fencing warning, and leave
92
+ `evals.seeding` off after snapshot evals. See [Deploy](../DEPLOY.md#strict-config-and-insika-doctor).
91
93
  3. One authenticated turn:
92
94
 
93
95
  ```bash