insika 0.9.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 (257) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +237 -2
  3. data/README.md +5 -3
  4. data/bin/insika +3 -0
  5. data/bin/insika-router +0 -1
  6. data/docs/AGENTS.md +90 -7
  7. data/docs/ARCHITECTURE.md +12 -7
  8. data/docs/ARTIFACTS.md +33 -3
  9. data/docs/BENCHMARK.md +50 -7
  10. data/docs/CONTEXT.md +63 -0
  11. data/docs/CROSS-HARNESS-BENCH.md +157 -0
  12. data/docs/DEPLOY.md +57 -11
  13. data/docs/EMBEDDING.md +10 -0
  14. data/docs/EVALS.md +33 -4
  15. data/docs/FACTS.md +8 -6
  16. data/docs/HARVEST.md +8 -3
  17. data/docs/KNOWLEDGE.md +7 -0
  18. data/docs/MEDIA.md +6 -2
  19. data/docs/OBSERVABILITY.md +143 -22
  20. data/docs/PERFORMANCE.md +110 -0
  21. data/docs/PLUGINS.md +4 -4
  22. data/docs/POLICY.md +22 -13
  23. data/docs/RELEASING.md +5 -0
  24. data/docs/ROUTER.md +7 -0
  25. data/docs/RUBYLLM_2_MIGRATION.md +307 -0
  26. data/docs/SECURITY.md +7 -0
  27. data/docs/SHARED-CONVERSATIONS.md +40 -0
  28. data/docs/SKILLS.md +5 -3
  29. data/docs/STUDIO.md +122 -0
  30. data/docs/TOOLS.md +111 -9
  31. data/docs/operate.md +3 -0
  32. data/lib/insika/agent_defaults.rb +140 -0
  33. data/lib/insika/agent_file_store.rb +12 -0
  34. data/lib/insika/agent_profile.rb +78 -8
  35. data/lib/insika/artifact_signing.rb +11 -0
  36. data/lib/insika/chat_builder.rb +79 -25
  37. data/lib/insika/checkpoint.rb +6 -1
  38. data/lib/insika/checkpoint_store.rb +59 -7
  39. data/lib/insika/commands/agent_payload.rb +3 -3
  40. data/lib/insika/commands/delete_kit.rb +26 -0
  41. data/lib/insika/commands/gate_harvest.rb +3 -0
  42. data/lib/insika/commands/review_guardrail_flag.rb +38 -0
  43. data/lib/insika/commands/run_harvest.rb +17 -1
  44. data/lib/insika/commands/seed_session.rb +11 -1
  45. data/lib/insika/commands/send_message.rb +9 -3
  46. data/lib/insika/commands/update_settings.rb +23 -1
  47. data/lib/insika/commands/write_kit.rb +27 -0
  48. data/lib/insika/compaction.rb +33 -7
  49. data/lib/insika/config_store.rb +87 -7
  50. data/lib/insika/context/priority.rb +3 -0
  51. data/lib/insika/context/provider.rb +2 -2
  52. data/lib/insika/context/providers/knowledge.rb +25 -2
  53. data/lib/insika/context/providers/memory.rb +53 -4
  54. data/lib/insika/context/providers/pending_confirmation.rb +52 -0
  55. data/lib/insika/context/providers/prompt.rb +17 -4
  56. data/lib/insika/context/providers/session.rb +39 -3
  57. data/lib/insika/context/reranker.rb +75 -0
  58. data/lib/insika/distill.rb +22 -13
  59. data/lib/insika/doctor.rb +34 -4
  60. data/lib/insika/dsl/runtime.rb +36 -4
  61. data/lib/insika/dsl/server_boot.rb +4 -12
  62. data/lib/insika/dsl.rb +13 -5
  63. data/lib/insika/edge_limiter.rb +23 -15
  64. data/lib/insika/egress_guard.rb +11 -3
  65. data/lib/insika/env_schema.rb +33 -73
  66. data/lib/insika/evals/assertions.rb +171 -16
  67. data/lib/insika/evals/golden.rb +141 -4
  68. data/lib/insika/evals/judge.rb +19 -10
  69. data/lib/insika/evals/moderator_compare.rb +47 -0
  70. data/lib/insika/evals/report.rb +61 -9
  71. data/lib/insika/evals/runner.rb +44 -4
  72. data/lib/insika/evals/simulator.rb +10 -1
  73. data/lib/insika/evals/transport.rb +200 -0
  74. data/lib/insika/event_stream.rb +63 -7
  75. data/lib/insika/evidence.rb +29 -8
  76. data/lib/insika/executor.rb +668 -141
  77. data/lib/insika/harvest.rb +26 -14
  78. data/lib/insika/harvest_engine.rb +1 -0
  79. data/lib/insika/harvest_store.rb +12 -0
  80. data/lib/insika/http_client.rb +87 -4
  81. data/lib/insika/kits.rb +54 -0
  82. data/lib/insika/knowledge.rb +34 -20
  83. data/lib/insika/llm_configurator.rb +46 -4
  84. data/lib/insika/llm_provider_store.rb +1 -3
  85. data/lib/insika/llm_trace_store.rb +54 -0
  86. data/lib/insika/mcp_client.rb +27 -63
  87. data/lib/insika/mcp_live_tool.rb +55 -9
  88. data/lib/insika/mcp_store.rb +32 -1
  89. data/lib/insika/mcp_tool_registry.rb +69 -26
  90. data/lib/insika/media.rb +38 -25
  91. data/lib/insika/memory_store.rb +4 -3
  92. data/lib/insika/memory_trim.rb +60 -0
  93. data/lib/insika/middleware.rb +12 -3
  94. data/lib/insika/model_metrics_store.rb +285 -0
  95. data/lib/insika/model_registry.rb +110 -0
  96. data/lib/insika/model_resolver.rb +1 -1
  97. data/lib/insika/model_selection.rb +14 -21
  98. data/lib/insika/model_visible.rb +17 -6
  99. data/lib/insika/onboarding.rb +1 -0
  100. data/lib/insika/outcome_store.rb +1 -1
  101. data/lib/insika/overlay_tool_registry.rb +25 -2
  102. data/lib/insika/pack_importer.rb +54 -9
  103. data/lib/insika/packaging.rb +14 -5
  104. data/lib/insika/pending_action_store.rb +45 -18
  105. data/lib/insika/plugin/loader.rb +4 -2
  106. data/lib/insika/profile_source.rb +4 -0
  107. data/lib/insika/provider_error_classifier.rb +18 -3
  108. data/lib/insika/queue_policy.rb +7 -2
  109. data/lib/insika/refinement/proposer.rb +42 -17
  110. data/lib/insika/reliability.rb +12 -3
  111. data/lib/insika/router/app.rb +14 -5
  112. data/lib/insika/router/session_key.rb +35 -5
  113. data/lib/insika/safety/config.rb +20 -3
  114. data/lib/insika/safety/corpus.rb +10 -3
  115. data/lib/insika/safety/factory.rb +39 -1
  116. data/lib/insika/safety/input_guardrail.rb +21 -1
  117. data/lib/insika/safety/judge_moderator.rb +70 -0
  118. data/lib/insika/safety/output_validator.rb +29 -5
  119. data/lib/insika/safety/prompt_echo.rb +89 -0
  120. data/lib/insika/schema_guard.rb +53 -76
  121. data/lib/insika/server/app.rb +17 -8
  122. data/lib/insika/server/responses.rb +19 -1
  123. data/lib/insika/session_actor.rb +58 -28
  124. data/lib/insika/session_store.rb +159 -10
  125. data/lib/insika/settings_store.rb +47 -1
  126. data/lib/insika/shared_conversations.rb +305 -0
  127. data/lib/insika/skill_catalog.rb +28 -2
  128. data/lib/insika/skill_store.rb +6 -0
  129. data/lib/insika/soak/runner.rb +1 -1
  130. data/lib/insika/steer_injector.rb +7 -48
  131. data/lib/insika/store.rb +29 -1
  132. data/lib/insika/stores/json_value.rb +37 -0
  133. data/lib/insika/stores/memory.rb +22 -35
  134. data/lib/insika/stores/postgres.rb +295 -0
  135. data/lib/insika/stores/sqlite.rb +257 -43
  136. data/lib/insika/stores/turn_counter.rb +25 -0
  137. data/lib/insika/studio/app.rb +1453 -161
  138. data/lib/insika/studio/assets/dist/application.css +1 -1
  139. data/lib/insika/studio/assets/dist/application.js +25 -25
  140. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  141. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  142. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  143. data/lib/insika/studio/assets/dist/fonts/ibm-plex-sans-latin-wght-normal.woff2 +0 -0
  144. data/lib/insika/studio/forms.rb +171 -9
  145. data/lib/insika/studio/nav_icons.rb +3 -0
  146. data/lib/insika/studio/views/_agent_config_retrieval.erb +73 -0
  147. data/lib/insika/studio/views/_agent_picker.erb +15 -0
  148. data/lib/insika/studio/views/_agent_tab_config.erb +78 -242
  149. data/lib/insika/studio/views/_agent_tab_history.erb +56 -1
  150. data/lib/insika/studio/views/_agent_tab_performance.erb +34 -0
  151. data/lib/insika/studio/views/_agent_tab_prompts.erb +7 -9
  152. data/lib/insika/studio/views/_agent_tab_skills.erb +16 -7
  153. data/lib/insika/studio/views/_agent_tab_tools.erb +90 -0
  154. data/lib/insika/studio/views/_agents_master.erb +8 -12
  155. data/lib/insika/studio/views/_cfg_alerts.erb +4 -0
  156. data/lib/insika/studio/views/_cfg_budget.erb +20 -0
  157. data/lib/insika/studio/views/_cfg_distill.erb +26 -0
  158. data/lib/insika/studio/views/_cfg_followup.erb +30 -0
  159. data/lib/insika/studio/views/_cfg_grounding.erb +21 -0
  160. data/lib/insika/studio/views/_cfg_guardrails.erb +47 -0
  161. data/lib/insika/studio/views/_cfg_harvest.erb +35 -0
  162. data/lib/insika/studio/views/_cfg_reliability.erb +32 -0
  163. data/lib/insika/studio/views/_chats_master.erb +66 -0
  164. data/lib/insika/studio/views/_customers_master.erb +37 -0
  165. data/lib/insika/studio/views/_empty.erb +15 -0
  166. data/lib/insika/studio/views/_facts_master.erb +42 -0
  167. data/lib/insika/studio/views/_harvest_candidate_detail.erb +14 -0
  168. data/lib/insika/studio/views/_identity.erb +16 -0
  169. data/lib/insika/studio/views/_kpi.erb +17 -0
  170. data/lib/insika/studio/views/_message.erb +40 -2
  171. data/lib/insika/studio/views/_model_chart.erb +82 -0
  172. data/lib/insika/studio/views/_refinement_master.erb +38 -0
  173. data/lib/insika/studio/views/{tool_edit.erb → _tool_edit.erb} +32 -9
  174. data/lib/insika/studio/views/_tool_users.erb +17 -0
  175. data/lib/insika/studio/views/_why.erb +53 -0
  176. data/lib/insika/studio/views/agent_detail.erb +17 -10
  177. data/lib/insika/studio/views/agents.erb +28 -16
  178. data/lib/insika/studio/views/approvals.erb +16 -11
  179. data/lib/insika/studio/views/artifact.erb +20 -7
  180. data/lib/insika/studio/views/artifacts.erb +19 -14
  181. data/lib/insika/studio/views/chats.erb +22 -29
  182. data/lib/insika/studio/views/customer.erb +88 -43
  183. data/lib/insika/studio/views/customers.erb +16 -26
  184. data/lib/insika/studio/views/evals.erb +49 -31
  185. data/lib/insika/studio/views/fact_detail.erb +114 -0
  186. data/lib/insika/studio/views/facts.erb +15 -116
  187. data/lib/insika/studio/views/followups.erb +63 -52
  188. data/lib/insika/studio/views/funnel.erb +104 -43
  189. data/lib/insika/studio/views/harvest.erb +85 -59
  190. data/lib/insika/studio/views/home.erb +138 -23
  191. data/lib/insika/studio/views/inbox.erb +36 -0
  192. data/lib/insika/studio/views/kits.erb +176 -0
  193. data/lib/insika/studio/views/knowledge.erb +92 -80
  194. data/lib/insika/studio/views/layout.erb +42 -11
  195. data/lib/insika/studio/views/live.erb +45 -0
  196. data/lib/insika/studio/views/mcp.erb +43 -13
  197. data/lib/insika/studio/views/models.erb +187 -0
  198. data/lib/insika/studio/views/parity.erb +38 -32
  199. data/lib/insika/studio/views/playground.erb +13 -9
  200. data/lib/insika/studio/views/refinement.erb +15 -224
  201. data/lib/insika/studio/views/refinement_detail.erb +243 -0
  202. data/lib/insika/studio/views/session.erb +152 -117
  203. data/lib/insika/studio/views/settings.erb +86 -22
  204. data/lib/insika/studio/views/skills.erb +53 -19
  205. data/lib/insika/studio/views/system_files.erb +7 -7
  206. data/lib/insika/studio/views/task.erb +45 -15
  207. data/lib/insika/studio/views/tasks.erb +35 -18
  208. data/lib/insika/studio/views/tools.erb +62 -73
  209. data/lib/insika/task_actor.rb +4 -1
  210. data/lib/insika/task_store.rb +67 -1
  211. data/lib/insika/telemetry/pricing.rb +16 -14
  212. data/lib/insika/telemetry/recorder.rb +103 -9
  213. data/lib/insika/telemetry/ruby_llm_instrumenter.rb +110 -0
  214. data/lib/insika/telemetry.rb +12 -2
  215. data/lib/insika/templates/daily-digest/README.md +13 -0
  216. data/lib/insika/templates/daily-digest/agent.rb +22 -11
  217. data/lib/insika/testing/store_contract.rb +65 -0
  218. data/lib/insika/text_tool_call.rb +66 -0
  219. data/lib/insika/tick.rb +7 -1
  220. data/lib/insika/token_store.rb +5 -0
  221. data/lib/insika/tool_batch.rb +1 -1
  222. data/lib/insika/tool_catalog.rb +12 -2
  223. data/lib/insika/tool_definition.rb +10 -4
  224. data/lib/insika/tool_envelope.rb +165 -28
  225. data/lib/insika/tool_manifest.rb +1 -0
  226. data/lib/insika/tool_store.rb +3 -0
  227. data/lib/insika/tool_trace_store.rb +15 -8
  228. data/lib/insika/tool_usage_report.rb +2 -2
  229. data/lib/insika/tools/a2a_remote.rb +1 -1
  230. data/lib/insika/tools/agent_enum.rb +1 -1
  231. data/lib/insika/tools/confirm_pending.rb +112 -0
  232. data/lib/insika/tools/data_defined_tool.rb +23 -15
  233. data/lib/insika/tools/generate_image.rb +2 -2
  234. data/lib/insika/tools/load_knowledge.rb +7 -3
  235. data/lib/insika/tools/load_skill.rb +3 -3
  236. data/lib/insika/tools/present.rb +1 -8
  237. data/lib/insika/tools/remember.rb +7 -2
  238. data/lib/insika/tools/run_persona_eval.rb +3 -5
  239. data/lib/insika/tools/save_artifact.rb +16 -6
  240. data/lib/insika/tools/schedule_followup.rb +3 -3
  241. data/lib/insika/tools/stuck_signal.rb +3 -3
  242. data/lib/insika/tools/subagent.rb +5 -5
  243. data/lib/insika/tools/subagents.rb +3 -3
  244. data/lib/insika/tools/tool_search.rb +7 -5
  245. data/lib/insika/tools/tts.rb +2 -2
  246. data/lib/insika/tools/update_briefing.rb +4 -4
  247. data/lib/insika/turn_output.rb +2 -2
  248. data/lib/insika/turn_state.rb +18 -0
  249. data/lib/insika/turn_timing.rb +51 -14
  250. data/lib/insika/version.rb +1 -1
  251. data/lib/insika/wiring/graph.rb +81 -16
  252. data/lib/insika/workflow.rb +25 -64
  253. data/lib/insika.rb +19 -3
  254. metadata +69 -13
  255. data/lib/insika/commands/import_mcp_tools.rb +0 -48
  256. data/lib/insika/mcp_http_client.rb +0 -67
  257. data/lib/insika/mcp_tool_ingestor.rb +0 -165
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec89eb1dae94e5a57733d0dd88a19d92ff98eb7e2ae0b907792e2a4ce8d508aa
4
- data.tar.gz: cb882c8015e00c0c905e5945a05a5a465c938dedda762914c3737a64a9f0d4fb
3
+ metadata.gz: b98a9d84df7cda373881cc11d0658bc3dd208ceb4bc52c23ed4fed4bd641fbb4
4
+ data.tar.gz: cecc0e4671a335efc5670f297aaf53ebafd5f1662f25a3121894825582494b30
5
5
  SHA512:
6
- metadata.gz: e2acb07f2ae896ee762cd3f1113dca3d462f80ea0bb32caea69f5db3fd6fbd31821cf9a5140b32c64c26b702b29b78aaf9fec0ca8f59653178dcb37afe9eca38
7
- data.tar.gz: f8c98a2b76c802a75e18a77e0762ef42c650aa956afb3f131e1e9ccacfb2a56de87540b7389c740021008b45a174fa371d7a6dbb0a5a7cf83f248ce41b696770
6
+ metadata.gz: 332d1cea74cdbee9e3241dfefc73538824643fe3b65964fb2584ed57fb4d85c7b1cb547c7d2ccf3ff2acd4893d49063bed60fbf6e94ed6968e1c30bbd9671b30
7
+ data.tar.gz: d203a16b2954fb36829ed074a463ebac6dce26fea980e546debf5e908e4bf6bfb77c7d04b2e703816254022eaf3da9dbbce1a09c845525454dc6126996ce00b0
data/CHANGELOG.md CHANGED
@@ -3,11 +3,246 @@
3
3
  All notable changes to this project are documented here.
4
4
 
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
6
- project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html) once
7
- it is released. Entries land with the pull request that makes the change.
6
+ project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) from 1.0.0 on. Entries land with the pull request that makes the change.
8
7
 
9
8
  ## [Unreleased]
10
9
 
10
+ ## [1.0.1] - 2026-10-23
11
+
12
+ ### Removed
13
+
14
+ - Internal planning notes (`docs/superpowers/`) are no longer in the repository or the
15
+ gem. 1.0.0 shipped them by mistake; the packaging now never ships that directory.
16
+
17
+ ## [1.0.0] - 2026-10-23
18
+
19
+ ### Added
20
+
21
+ - Harvest page: each pending or blocked candidate shows its skill text, rationale and origin conversation; a blocked one can be re-gated once the missing piece is fixed; the page says when the loop skips an agent (harvest off, or no grounding matcher).
22
+ - Studio screens for watching conversations and what waits on a person (see
23
+ [Studio](docs/STUDIO.md)):
24
+ - **Live** (`/studio/live`): one card per recent conversation, moved by the
25
+ event stream (tools in the running turn, "agent is writing…", guardrail
26
+ flags), with an event ticker. `?agent=` shows one agent's conversations.
27
+ - **Why this reply**: under each agent reply, its turn's context by category,
28
+ skills injected and why, tool calls with timing and guardrail flags, plus
29
+ Replay in Playground and Save as eval case.
30
+ - **Inbox** (`/studio/inbox`): approvals, fact proposals, mined skills,
31
+ refinement runs and guardrail false-positive reports in one list, oldest
32
+ first, with tabs per type.
33
+ - **Guardrail false positives**: a flag can be marked as a false positive,
34
+ then resolved or undone (`:review_guardrail_flag`). The verdict lives on the
35
+ flag inside the session.
36
+ - **Agent History**: outcomes per day over 30 days with a marker per change
37
+ to a prompt file or to the agent's own skills, and per change a short diff
38
+ and the outcomes 7 days after vs 7 days before.
39
+ - **Funnel drops**: the biggest loss between stages, and per loss the
40
+ conversations that stopped there, grouped by the last thing that happened.
41
+ - Chats tabs (All / Active now / Guardrail flagged), a customer card on the
42
+ session, a tool-call waterfall per turn, an Overview "Needs attention" row,
43
+ a `⌘K` palette, and a sidebar grouped as Operate / Review / Build /
44
+ Insights / Platform with a count badge on each Review page.
45
+ - Markup tags in message text (`<carrossel/>`, `<vitrine formato="lista">`)
46
+ show as highlighted tokens, escaped and never rendered.
47
+ - `Store#amend(scope, key, value)`: overwrites a key without moving it in
48
+ `#recent`. The default is `#set`; a backend whose `#recent` follows its own
49
+ write order overrides both (the contract suite checks it). Memory, SQLite and
50
+ Postgres implement it.
51
+ - Persisted turn messages and tool-call traces carry `task_id`, so the Studio
52
+ ties each message to its own turn.
53
+
54
+ - `scripts/migrate_openclaw.rb seed`: carries recent OpenClaw conversations into
55
+ Insika sessions (`POST /v1/conversations/:id/seed`) so a returning customer
56
+ keeps the context. Reads root and per-process SQLite transcripts, follows the
57
+ active branch from the last compaction, keeps only customer text and agent
58
+ replies, and skips conversations that already have messages. `--only`/`--as`
59
+ rehearse one conversation on a test chat.
60
+ Each session is stamped with `--agent`, so the Studio lists it under that
61
+ agent; re-running stamps sessions seeded before (they still answer 409).
62
+ - Platform agent defaults (Studio › Settings › Agent defaults, or
63
+ `update_settings` with `agent_defaults`): an agent with no value of its own
64
+ for reliability, knowledge, memory retrieval, budget, alerts, guardrails,
65
+ grounding, follow-ups, distillation or harvest runs with the platform's. The
66
+ agent's own value wins whole. Each of those sections in the agent config has
67
+ a "use the platform default" box; saving with it checked keeps the agent
68
+ inheriting. A default is validated on save, and one that stops building
69
+ later leaves agents on their own values instead of failing turns. Agents
70
+ saved in the Studio before this have their own knowledge, guardrails,
71
+ distillation and harvest values (the form always wrote them): check the box
72
+ to make them inherit.
73
+
74
+ - **Optional Postgres store** (`INSIKA_DATABASE_URL`, `INSIKA_DATABASE_POOL`). It passes the
75
+ same store contract as SQLite; `pg` is not a gem dependency and loads only when the
76
+ variable is set. `scripts/store_copy.rb` moves an existing store. See
77
+ [Postgres store](docs/DEPLOY.md#postgres-store-optional) for when it pays off.
78
+ - `Store#entries` and `Store#recent`: a whole scope, or its most recently written keys, in
79
+ one call. The default implementation keeps third-party backends working; SQLite and
80
+ Postgres answer each in one query. The Studio's session and task lists use them.
81
+ - `INSIKA_SLOW_STORE_MS` also logs each slow write batch: its size, bytes, scopes, and how
82
+ long it waited for the lock versus how long its commit took.
83
+
84
+ ### Changed
85
+ - Session stats are versioned (`"v" => 2`, adds the guardrail flagged and
86
+ reported counts). `scripts/backfill_session_stats.rb` now also rewrites stats
87
+ older than the current version, not only missing ones.
88
+ - Reviewing a guardrail flag no longer moves the session to the top of the
89
+ recent lists.
90
+ - **MCP tools are named after their instance**: `<instance>__<tool>` (e.g.
91
+ `shop_staging__list_products`). Two instances offering the same tool used to share
92
+ one name, and every call went to whichever instance came first. Agents that allowed
93
+ MCP tools by name must select them again; per-tool overrides keep the server's name.
94
+ - `INSIKA_EGRESS_HOSTS` pins an MCP server only when its host resolves to a private
95
+ address. The operator registers the URL and the model cannot change it, so adding a
96
+ public server needs no redeploy. HTTPS still applies, and a private server still needs
97
+ `INSIKA_EGRESS_ALLOW_PRIVATE` and a listed host.
98
+
99
+ - An input guardrail injection hit no longer replaces the turn with the canned
100
+ refusal by default. The message reaches the model with a per-turn
101
+ `<security_notice>` (never persisted) and an audit `guardrail_flagged` with
102
+ `action: "flag"`, so a legitimate request in the same message is still served.
103
+ Sexual and abuse hits still block, now also when the message carries an
104
+ injection too. `guardrails.injection: "block"` (Studio › Guardrails › on
105
+ injection) restores the old behavior.
106
+ - `Store#recent` takes an optional fourth argument, `offset`, to read the next page; a
107
+ backend that implements the three-argument form keeps working (the stores pass
108
+ `offset` only when paging). SQLite reads a page's keys from the index and then only
109
+ their values, instead of reading every value in the range to sort it.
110
+ - Studio: the tasks list shows 50 tasks per page, newest first, instead of every task.
111
+ The home reads only the sessions of the last 14 days; its messages card counts that
112
+ window (and says so), and the conversations total comes from the keys alone.
113
+ - Every session write also writes a small stats record (last update, message count,
114
+ agent) in the `session_stats` scope, and the Studio home reads those instead of whole
115
+ sessions (every stat, filtered by date, so the order they were written in does not
116
+ matter). Sessions stored before this change have no stats until
117
+ `scripts/backfill_session_stats.rb` runs once (idempotent, safe with the app running;
118
+ it also removes stats whose session is gone).
119
+ - RubyLLM is pinned to a newer `2.0.0` commit: streamed responses are priced once when
120
+ read instead of on every chunk, and HTTP connections are shared per process and
121
+ settings. Specs stub LLM HTTP with `stub_llm_http`, because swapping the adapter on an
122
+ already-built connection no longer takes effect.
123
+
124
+ - Studio tools are grouped by origin and MCP server, with name/server filtering,
125
+ collapsible sections and per-section select/deselect actions. Selection keeps
126
+ denied tools blocked and requires saving before permissions change.
127
+
128
+ - Workflow and tool argument validation now use JSONSchemer. Explicit nulls and
129
+ declared constraints are enforced; existing tool scalar leniency remains.
130
+ - Distillation, harvest, knowledge and refinement use native structured responses.
131
+ Retired MCP import delegates to live discovery instead of generating HTTP snapshots.
132
+
133
+ - RubyLLM 2 migration: native loop control, tool schemas, cache markers, and usage
134
+ readers. MCP now uses the native RubyLLM client, replacing `ruby_llm-mcp` and the
135
+ Origin-header patch. Legacy SSE requires migration to Streamable HTTP. See
136
+ [migration results](docs/RUBYLLM_2_MIGRATION.md).
137
+ - Requires RubyLLM `~> 2.1` from RubyGems; the Git pin is gone. A provider's 400
138
+ quota message ("quota exceeded", "wait before trying again") now arrives as
139
+ `RubyLLM::RateLimitError` and is retried and rotated instead of failing the turn.
140
+
141
+ - SQLite writes from concurrent turns share one transaction and one commit (group commit),
142
+ so N worker processes take the file's write lock far less often. A turn opens and
143
+ closes its task record in one write each, and skips an empty evidence write.
144
+ - Litestream 0.5 with its blocking checkpoint disabled (`truncate-page-n: 0`); it restores
145
+ backups made by 0.3.
146
+ - Less CPU per turn: the prompt reads an agent's files once per build, the prompt-echo
147
+ check caches each squeezed prompt, and streamed events are matched only against their
148
+ own session's subscriptions.
149
+
150
+ ### Removed
151
+
152
+ - **The `HARNESS_*` environment variables (breaking).** Only the `INSIKA_*` names are
153
+ read. A leftover `HARNESS_*` key is no longer honored: it is reported as an error
154
+ naming its `INSIKA_*` replacement (a warning at boot, fatal under
155
+ `INSIKA_CONFIG_STRICT`, and an error in `insika doctor`), so a stale `HARNESS_DB`
156
+ cannot silently boot the engine on the memory backend. `EnvSchema.reconcile_legacy!`
157
+ is gone, and the bundled `insika-code` plugin and example read `INSIKA_CODE_*`.
158
+ - **The `plugin.yml` manifest name (breaking).** Only `insika.plugin.yml` loads. A
159
+ leftover `plugin.yml` is not loaded and warns to rename it.
160
+ - `Insika::LLMHTTPAdapter` (`faraday_adapter = :insika_net_http`). RubyLLM 2.1 shares
161
+ one connection per process and settings, so the stock `:net_http` no longer builds a
162
+ CA store per chat. A deployment that set `:insika_net_http` drops that line.
163
+
164
+ ### Fixed
165
+
166
+ - Skills written in one worker (pack import, Studio edit, `write_skill`) now reach the other workers within 2 s. The catalog only reloaded in the worker that handled the write, so the others kept the skills they booted with until a restart — in the Studio and in customer turns.
167
+ - Harvest no longer mines simulator/eval sessions (`sim-…`, `eval-…`), and the miner sees the candidates still open so it stops re-proposing the same idea under new names.
168
+ - Harvest: a bare `miner.model` ref (no `provider/`) now uses the agent's provider; it used to fail every run with `Provider must be specified`.
169
+ - Studio: `/studio` without a trailing slash answered 404; it now opens the
170
+ Overview.
171
+ - Studio: the agent History chart and arm conversions counted outcome kinds
172
+ against the primary stage name, so they read 0 whenever the kind and stage
173
+ names differ; they now map kinds through `advance_on`.
174
+ - Studio: a negative message or flag index on a guardrail review counted from
175
+ the end and reviewed the wrong flag; it is now not found.
176
+ - An error that is fatal only for one model or account no longer kills the turn:
177
+ no balance (402), a bad key or missing access (401/403) and a model the provider
178
+ does not have (404) move straight to the next fallback node — no retry on that
179
+ node, no backoff, and no circuit-breaker count (an open primary circuit fails
180
+ fast without rotating). On the last node it still raises; a 400 still never
181
+ rotates.
182
+ - Studio model fields are type-to-search pickers over the configured providers'
183
+ `provider/model` refs (their declared models, plus the chat models of key-only
184
+ providers): the agent model, a new agent, the playground pin and Settings'
185
+ default, fallback (one slot per position) and utility models. The server
186
+ refuses a ref outside the list — a typed `openai/gpt-x` under provider `openai`
187
+ used to reach the API as `openai/openai/gpt-x`. A stored value outside the list
188
+ is shown marked "not configured" and may be saved back unchanged. The plain
189
+ `model`/`provider` fields still pass as given for API clients.
190
+ The same pickers cover the reliability fallback chain (agent and Agent
191
+ defaults) and the refinement proposers, one slot per position, and the
192
+ knowledge, distill, harvest-miner and compaction models.
193
+ - Data-tools imported or edited in one worker now reach the others: the tool
194
+ registry checks the config version token (at most every 2 s) and rebuilds when
195
+ it moved, and the tool catalog follows. Before, the other workers kept the
196
+ catalog they booted with until a restart.
197
+ - LLM providers authored at runtime reach every worker process, and survive a
198
+ restart: each chat re-applies the stored providers when they changed. Before,
199
+ only the worker that served the edit had the key, so a fallback on that
200
+ provider failed as unconfigured. A cleared key stops being used; a key from
201
+ the environment is never cleared by it.
202
+ - Deleting an LLM provider authored at runtime puts back the key and base the
203
+ process started with (for example from the environment) instead of leaving
204
+ the provider without credentials; a worker that never applied the provider
205
+ leaves its config alone. Delete no longer disables a provider that also has a
206
+ key from the environment.
207
+ - Native chat usage includes tool rounds and retries, and failed turns expose
208
+ recorded usage. Responses projects cache-inclusive totals; negotiated pricing
209
+ no longer subtracts cache reads from already non-cached input.
210
+ - Reliability honors session model pins and model allowlists without multiplying
211
+ its attempt budget by RubyLLM transport retries.
212
+ - Speech response reads stop at the byte limit before buffering the entire body.
213
+ - Workflow schemas accept valid `false` values.
214
+
215
+ - **A merged fragment is framed by `steer_join`, like a steered one already was.** The
216
+ two burst doors disagreed on what the model sees: a steered message becomes its OWN
217
+ `user` message in the transcript (framed by `steer_join`), while a merged one was
218
+ concatenated into the queued turn's single payload by a bare `\n`. A burst therefore
219
+ reached the model as one run-on paragraph with no boundary between the asks, and a
220
+ trailing constraint could be answered by nothing (staging, Acme Beauty,
221
+ 2026-09-18: five messages merged 4/4, and the answer never mentioned the fifth).
222
+ `steer_join` now applies to both doors; the default `nil` returns the text untouched,
223
+ so a wiring that never set it is unchanged.
224
+
225
+ - **The door is open for as long as a turn is queued, not only during a debounce
226
+ window.** `collect`/`steer` merged a fragment only while a turn sat in a
227
+ `debounce_ms` window; a message that arrived while a turn waited behind the one in
228
+ flight joined nothing and became a turn of its own, third in line. A
229
+ `POST /v1/messages?stream=false` caller then held its connection through TWO turns
230
+ and timed out — on the WhatsApp integration that surfaced as a technical apology
231
+ to the customer, once per excess message of a burst. A queued turn is now mergeable
232
+ with or without a window, so every burst message answers immediately with `merged`
233
+ or `steered`. `followup` (the default) is untouched.
234
+ - **Re-importing a pack no longer erases the knobs it did not send.** `limits` and
235
+ `params` now MERGE per key on an update (`PackImporter::KNOB_BAGS`); everything
236
+ else keeps replacing. A provisioning client that publishes
237
+ `limits: {turn_timeout:, context_budget:}` used to drop every other knob on the
238
+ agent — `queue_mode`, `debounce_ms`, `chat_rate_limit` — so an unrelated playbook
239
+ publish silently reset the queue policy an operator had set in the Studio. The
240
+ authoritative allowlists (`prompt_files`/`skills`/`tools_allow`) are unchanged: a
241
+ name that left the pack still leaves the agent.
242
+ - One door per queued turn: with two turns waiting, the second used to take over the
243
+ first's fragment count, and the first then ran from a message snapshot the store had
244
+ already grown past.
245
+
11
246
  ## [0.9.0] - 2026-09-07
12
247
 
13
248
  The commerce-agent wave: the engine now enforces what the prompt used to ask for —
data/README.md CHANGED
@@ -16,7 +16,7 @@ web control UI. Point an existing Responses client at it and serve many agents f
16
16
  deployment.
17
17
 
18
18
  - **Drop-in `/v1/responses`** — SSE streaming and usage, the shape existing clients already speak.
19
- - **Durable & resumable** — every turn checkpoints; a crash resumes without repeating side-effects.
19
+ - **Durable & resumable** — every turn checkpoints; recorded side-effect calls are deduplicated on replay. External writes still need idempotency.
20
20
  - **Agents as data** — agents, prompts, skills and tools are editable at runtime (UI or API), no redeploy.
21
21
  - **Tools & skills** — code, HTTP data and live MCP tools; skills load on demand.
22
22
  - **Evidence-backed actions** — ID checks before writes, serial side effects per session, and selected evidence cards ([Tools](docs/TOOLS.md)).
@@ -139,6 +139,7 @@ navigates by.
139
139
  - [Load test](docs/LOADTEST.md) — load-testing and data topology.
140
140
  - [Soak](docs/SOAK.md) — the long run that finds what a short one cannot.
141
141
  - [Benchmark](docs/BENCHMARK.md) — the neutral, reproducible, provider-free engine benchmark.
142
+ - [Performance](docs/PERFORMANCE.md) — what keeps a turn cheap (fibers, SQLite group commit, memory trim) and what was tried and left out.
142
143
 
143
144
  **Improve**
144
145
 
@@ -178,6 +179,7 @@ and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). Vulnerabilities go through
178
179
 
179
180
  ## Status
180
181
 
181
- Pre-1.0 — published on [RubyGems](https://rubygems.org/gems/insika); APIs may
182
- still change between minor versions ([CHANGELOG.md](CHANGELOG.md)). Licensed
182
+ Published on [RubyGems](https://rubygems.org/gems/insika) and follows
183
+ [Semantic Versioning](https://semver.org/) from 1.0.0: a breaking change only lands in
184
+ a new major version ([CHANGELOG.md](CHANGELOG.md)). Licensed
183
185
  **MIT** ([LICENSE](LICENSE)).
data/bin/insika CHANGED
@@ -505,6 +505,9 @@ def local_side_effect_tools(agent)
505
505
  profile = Insika::StoredProfileSource.new(config_store: config_store).fetch(agent)
506
506
  return nil if profile.nil?
507
507
 
508
+ # Kits add reachable tools at turn time; the swap list must count them too.
509
+ profile = Insika::Kits.apply(profile, Insika::SettingsStore.new(config_store: config_store).kits)
510
+
508
511
  registry = Insika::OverlayToolRegistry.new(
509
512
  base: Insika::ToolRegistry.new, tool_store: Insika::ToolStore.new(config_store: config_store),
510
513
  http: Insika::HttpClient.new
data/bin/insika-router CHANGED
@@ -34,7 +34,6 @@ module Insika
34
34
  module_function
35
35
 
36
36
  def main
37
- Insika::EnvSchema.reconcile_legacy!
38
37
  pool = build_pool
39
38
  app = Insika::Router::App.new(
40
39
  pool: pool,
data/docs/AGENTS.md CHANGED
@@ -132,7 +132,9 @@ block after the agent's identity in the system prompt: a weak or empty tool
132
132
  result means *try again with a different approach — a rephrased query, a
133
133
  synonym, a broader term — before telling the user you found nothing* (and don't
134
134
  narrate the retries); a tool error means *read it and fix the arguments*, never
135
- repeat the exact same call. Without it, a search that returns 0 results reads as
135
+ repeat the exact same call; and an action is *reported as done only after its
136
+ tool call returned success* — never before the call, never over a failed or
137
+ blocked one. Without it, a search that returns 0 results reads as
136
138
  final and the model answers "I couldn't find it" when a synonym one call away
137
139
  would have.
138
140
 
@@ -251,7 +253,7 @@ Which mode you want depends on **when** the message arrives:
251
253
  | `queue_mode` | The message arrives… | What happens |
252
254
  |---|---|---|
253
255
  | `followup` (default) | any time | it waits its turn in the queue — today's behavior, named |
254
- | `collect` | before the turn starts | the fragments merge into ONE turn |
256
+ | `collect` | before the turn starts — including while it waits behind another turn | the fragments merge into ONE turn |
255
257
  | `steer` | while the turn is running tools | it is appended to the run in flight |
256
258
  | `interrupt` | while a turn is running that is now **wrong** | that turn is abandoned; this message becomes its own turn |
257
259
 
@@ -270,6 +272,13 @@ limit :debounce_max_ms, 10_000 # ceiling on the total wait, so typing forever
270
272
  With those settings the three fragments above become one turn carrying
271
273
  `"oi\nqueria saber do pedido\n1234567"`, released 2 s after the last one.
272
274
 
275
+ **The window is not what opens the door.** A turn is "not started" from the moment
276
+ it is created until it runs, and that includes the whole time it waits behind the
277
+ turn in flight — so with `debounce_ms` at 0 a fragment still merges into a queued
278
+ turn. What the window buys is the case where nothing is running: it holds the turn
279
+ at the front of the queue so fragments have something to land on. Without it a
280
+ burst on an idle session becomes one turn per message, each answered in order.
281
+
273
282
  All three follow the platform-layer rule above, with one extra rung on top:
274
283
  **session vars → this agent's limits → the platform default (Studio, `queue.*`) →
275
284
  off**. Pinning `queue_mode` in a session's vars is how an operator takes one
@@ -304,11 +313,13 @@ separately lives in one event, emitted when the window closes:
304
313
  Times and counts, never content. That is what answers "the customer says they
305
314
  sent the order number" without keeping a throwaway task per fragment.
306
315
 
307
- > **`steer` also collects at the door.** The two windows are the same policy's
308
- > halves, not two modes: a `steer` agent that set a `debounce_ms` also merges the
309
- > fragments that land before the turn starts. The window value is what
310
- > an operator replaces the legacy pre-batch buffer with — `steer` catches anything
311
- > that arrives after the turn started, the door window the fragments before it.
316
+ > **`steer` also collects at the door.** The two are the same policy's halves, not
317
+ > two modes: a `steer` agent merges the fragments that land before the turn starts
318
+ > and appends the ones that land after it — `debounce_ms` only widens the first
319
+ > half. That pairing is what makes a burst answerable at all: whatever the customer
320
+ > sends either joins the run in flight (`steered`) or the turn behind it (`merged`),
321
+ > and the caller hears which one immediately instead of holding a connection open
322
+ > until someone else's turn ends.
312
323
 
313
324
  #### `steer` — the message arrives while the turn is already running
314
325
 
@@ -632,6 +643,24 @@ a running agent.
632
643
 
633
644
  Photos, voice notes and documents travel through the message contract as additive content parts, and the engine can hand generated images back the same way. See [Media](MEDIA.md).
634
645
 
646
+ ## Caller context next to the message
647
+
648
+ A caller that wraps each message with its own text (channel rules, the products
649
+ the customer was just shown) sends that text in `context`, not in `message`:
650
+
651
+ ```bash
652
+ curl -X POST /v1/messages?stream=false -H "Authorization: Bearer $TOKEN" \
653
+ -d '{ "agent": "store-support", "session_id": "chat-7",
654
+ "context": "<channel>sms</channel>", "message": "show me more" }'
655
+ ```
656
+
657
+ The model is asked `context`, a blank line, then `message`, and the transcript
658
+ keeps the same text. The input guardrail, retrieval and routing read `message`
659
+ alone, so a product name in `context` never blocks the customer, and the
660
+ customer cannot slip past the guardrail by typing a caller's tags. Optional
661
+ String; absent = the message alone. `POST /v1/responses` takes the same `context`
662
+ next to `input`.
663
+
635
664
  ## Customer-scoped memory and the right to be forgotten
636
665
 
637
666
  Memory is naturally **per customer, not per tenant**. A message that carries a
@@ -648,6 +677,10 @@ curl -X POST /v1/messages?stream=false -H "Authorization: Bearer $TOKEN" \
648
677
  tenant never see each other, and the `<request_context>` tenant label (the
649
678
  merchant) is untouched. Absent `customer` = today's per-tenant/per-chat
650
679
  behavior.
680
+ - **Display labels** — optional `customer_name` and `customer_phone` (Strings)
681
+ label the conversation in the Studio (the name replaces the `customer` key;
682
+ the phone shows beside the agent) and the Chats search matches both. They
683
+ follow the latest value sent and have no effect on memory scope.
651
684
  - **Right to be forgotten** — `POST /v1/commands/forget_customer` (operator)
652
685
  purges the customer's memory cell, their sessions and everything those
653
686
  sessions left behind — per-session traces, the tasks (the message text lives
@@ -733,11 +766,61 @@ Does each ID come from the server? Declare `evidence` on the lookup tool and
733
766
  Mark writes as `side_effect` so parallel batches serialize them and recovery skips
734
767
  completed calls. See [Tools](TOOLS.md#provenance-checking-ids-before-a-write).
735
768
 
769
+ ## Do not offer the irreversible action
770
+
771
+ `evidence` and `requires_evidence` stop a write built on an id the customer was never
772
+ shown. They do not stop a write the model decides the customer asked for. That is a
773
+ prompt problem, and the [cross-harness bench](CROSS-HARNESS-BENCH.md) measured how
774
+ large it is.
775
+
776
+ One task: the customer has two units in the cart and says *"adiciona logo por favor"* —
777
+ ambiguous, and the correct reading is a request to add something, not to check out.
778
+ Ten rounds per harness, on the same model:
779
+
780
+ | Harness | Passed | Mentioned closing the order | Called `create_order` |
781
+ | --- | --- | --- | --- |
782
+ | Hermes | 10/10 | 0/10 | 0 |
783
+ | Insika, reasoning off | 9/10 | 10/10 | 1 |
784
+ | Insika, reasoning medium | 7/10 | 10/10 | 3 |
785
+ | Pi | 5/10 | 9/10 | 5 |
786
+
787
+ Every harness that offered to close the order sometimes closed it instead, at rates
788
+ from 10% to 50%. The one that never offered never closed it. It read the ambiguous
789
+ line as a new request and asked *which product* — so checkout was never in the
790
+ sentence the model was continuing.
791
+
792
+ **The rule for a prompt:** when the customer's intent is ambiguous, ask about the
793
+ thing they named, and do not volunteer the irreversible next step in the same breath.
794
+ An action the reply never proposes is one the model cannot slip into performing. This
795
+ is a writing rule, not an engine setting; nothing in the engine needs to change for
796
+ it, and no `halt_when` or allowlist expresses it, because the tool call is legitimate
797
+ in every other turn of the same conversation.
798
+
799
+ **Its cost, before you copy it.** Asking *"which product?"* about an item already in
800
+ the cart ignores context the customer just gave, and reads as an agent that was not
801
+ listening. The bench rewards it because the bench grades the store's state. A shop
802
+ might reasonably prefer the warmer reply and the 10% slip, or split the difference:
803
+ confirm what is in the cart, and stop there — let the customer be the one who says
804
+ close it. What you should not do is confirm the cart and offer checkout in the same
805
+ message, which is the shape that produced every failure above.
806
+
807
+ Reasoning level does not fix this. The same three tasks fail at `medium` and at `off`;
808
+ across ten rounds the two settings contradict each other task by task.
809
+
810
+ **The engine's alternative.** If the shop wants the warmer reply *and* zero slips,
811
+ take the write out of the same turn as the offer: declare the tool under
812
+ [`customer_confirm`](TOOLS.md#customer-confirmation-a-write-the-conversation-approves).
813
+ The model may then propose closing the order as freely as it likes — the engine holds
814
+ the call, the reply asks, and only the customer's next message runs it. The cost moves
815
+ from the wording to the conversation: one extra turn on every legitimate close.
816
+
736
817
  ## See also
737
818
 
738
819
  - [Tools](TOOLS.md) — define, register, and troubleshoot tools.
739
820
  - [Artifacts](ARTIFACTS.md) — the report destination: the tool, the routes, the signed link.
740
821
  - [Skills](SKILLS.md) — progressive playbooks an agent loads on demand.
822
+ - [Cross-harness bench](CROSS-HARNESS-BENCH.md) — the same store, six harnesses, and
823
+ where the prompt rule above came from.
741
824
  - [Context](CONTEXT.md) — what fills a turn's prompt, and memory.
742
825
  - [Security](SECURITY.md) — guardrails, sandbox, approvals, edge limits.
743
826
  - [Architecture](ARCHITECTURE.md) — how a turn actually runs.
data/docs/ARCHITECTURE.md CHANGED
@@ -167,9 +167,11 @@ The order is not arbitrary:
167
167
  - **The initial checkpoint is written before the model call.** "The checkpoint of
168
168
  turn *n* holds the state at the *start* of turn *n*." Without it, a crash during
169
169
  the model call would orphan the task with no checkpoint — unrecoverable.
170
- - **Middleware wraps the model-facing stages**, so the edge limiter and input
171
- guardrail run *before* the provider is ever touched — a flood or an injection is
172
- refused without a paid model call.
170
+ - **Middleware wraps the chat stages.** For opt-in reranking, the leading edge
171
+ limiter also wraps context preparation: admission runs once before paid retrieval.
172
+ Context still precedes policy; the input guardrail and plugin middleware receive
173
+ the prepared context, including any budget warning, before chat runs. Turns with
174
+ lexical-only retrieval keep the original order.
173
175
  - **Persistence is a fixed order** (checkpoint → session → task) and a pure drain
174
176
  point: the last stage never suspends, so a checkpoint is never left half-written.
175
177
  - **Output validation** runs as an after-task hook on the produced content
@@ -210,7 +212,7 @@ flowchart TD
210
212
  ```
211
213
 
212
214
  HTTP data tools and presentation tools are both stored definitions. Presentation
213
- runs in-process; MCP remains a live server call (HTTP/SSE or stdio). HTTP egress
215
+ runs in-process; MCP remains a live server call (Streamable HTTP or stdio). HTTP egress
214
216
  is checked before a request. Tool errors reach the model as error results so it
215
217
  can recover; a provenance refusal reports `blocked` without contacting the backend
216
218
  or asking an operator. Completed side effects are recorded for resume.
@@ -256,9 +258,12 @@ flowchart LR
256
258
  completed --> done((( )))
257
259
  ```
258
260
 
259
- Resume always replays from the *start of the last checkpointed turn*. Tool calls
260
- that already completed in the interrupted turn are recorded in the checkpoint and
261
- **not** re-executed, so a non-idempotent side-effect fires at most once. A resumed
261
+ Resume always replays from the *start of the last checkpointed turn*. Side-effect
262
+ calls whose completion was recorded in the checkpoint are skipped when replayed
263
+ with the same call id. An external effect can succeed before that record is saved;
264
+ a crash or failed checkpoint write in that window can repeat the effect on resume.
265
+ Use destination-supported idempotency keys or reconciliation for those operations:
266
+ checkpointing alone does not provide exactly-once external execution. A resumed
262
267
  turn is also never re-counted against edge-limit ledgers. Cancellation is
263
268
  cooperative — checked at stage boundaries, never in the middle of a store write.
264
269
 
data/docs/ARTIFACTS.md CHANGED
@@ -63,9 +63,39 @@ Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src
63
63
  X-Content-Type-Options: nosniff
64
64
  ```
65
65
 
66
- No script, no external fetch, no forms. The model writes HTML with **inline
67
- SVG** for charts — that is a skill instruction (palette, tables, pure-SVG bars),
68
- not engine code. A "real" charting need is a plugin.
66
+ No script, no external fetch, no forms. The model writes self-contained HTML/CSS
67
+ with **inline SVG** or HTML bars for charts. Native radio controls and
68
+ `details`/`summary` still work inside this boundary.
69
+
70
+ ## Interactive executive reports
71
+
72
+ The tool description and `daily-digest` skill guide future HTML reports. Existing
73
+ artifacts keep their saved content. The
74
+ [example report](https://github.com/guizaols/insika/blob/main/examples/scheduled-report/interactive-report.html)
75
+ uses explicitly fictional data and works without JavaScript or dependencies.
76
+
77
+ - Lead with the title, reporting period, generation date and a concise takeaway
78
+ supported by the numbers. Include sources and limitations; mark missing data
79
+ and fictional examples honestly.
80
+ - Use warm neutral surfaces and one amber or merchant accent. Keep typography,
81
+ spacing and KPI cards consistent; support system dark mode and narrow screens.
82
+ - Label charts with units and exact values. Use responsive SVG or HTML bars and a
83
+ captioned table with column headers, so touch users can read every value.
84
+ - Switch Chart/Table with labeled, focusable native radios in a `fieldset` with
85
+ a `legend`, styled through CSS `:checked`. These are radios, not ARIA tabs.
86
+ - Use CSS hover/focus tooltips in reserved space, highlights, a visible 2px focus
87
+ outline, and `details`/`summary` for supporting evidence. Keep tooltips visible
88
+ when the pointer moves onto them. Do not make hover the only way to
89
+ read data or use `<title>` as the only tooltip.
90
+ - Limit color/opacity transitions to 140ms and disable them under
91
+ `prefers-reduced-motion: reduce`. Avoid decorative animation. Print styles
92
+ include the complete table and details regardless of selected/collapsed state,
93
+ hide controls and keep text legible; use the browser's print command.
94
+
95
+ Interactivity explores a **dated snapshot**. It does not fetch new data, recompute
96
+ the source or imply a live feed. A later report run saves a new artifact. Do not
97
+ add scripts, event handlers (including `onclick` print buttons), external resources
98
+ or form submissions to generated reports.
69
99
 
70
100
  ## Reasoning effort on a report turn
71
101
 
data/docs/BENCHMARK.md CHANGED
@@ -45,6 +45,7 @@ reports only the engine's contribution:
45
45
  | **gen** (p50/p95) | streaming the rest through the pipeline (filter/emit/event stream) |
46
46
  | **throughput** | turns/s a single process sustains at a given concurrency |
47
47
  | **pipeline overhead** | engine work per streamed token (µs) |
48
+ | **allocations** | Ruby objects allocated per measured turn, including benchmark driver bookkeeping, excluding warmup |
48
49
 
49
50
  **Out of scope, on purpose:** end-to-end latency, time-to-first-token *against a
50
51
  provider*, and tokens/s of *model generation*. Those are provider-bound — the
@@ -52,8 +53,9 @@ Insika has no lever on them — so this suite makes no claim about them.
52
53
 
53
54
  The stub implements exactly the chat surface the executor touches, and each turn
54
55
  runs the full engine path: context build, policy resolution, guardrail
55
- detectors, chat assembly, the tool-call round-trip, streamed output, persistence,
56
- checkpointing, and the event stream. Only the network call is removed.
56
+ detectors, chat assembly, tool-call callbacks, streamed output, persistence,
57
+ checkpointing, and the event stream. The default mode replaces RubyLLM's Chat;
58
+ use `--ruby-llm` below to include the real Chat and tool execution.
57
59
 
58
60
  ## Scenarios
59
61
 
@@ -62,8 +64,8 @@ imported the same way any pack is, so the measured path is the real one.
62
64
 
63
65
  - **greeting** — a minimal turn: a short system prompt, no tools. Baseline
64
66
  engine overhead.
65
- - **tool_call** — a turn where the agent calls one tool; exercises tool
66
- assembly, the wrap/dispatch path, and the call→result round-trip.
67
+ - **tool_call** — a turn simulating one tool call; exercises tool assembly and
68
+ call/result callbacks without executing the tool.
67
69
  - **multi_turn** — a one-shot carrying prior conversation messages; shows how
68
70
  overhead moves as the context the engine assembles grows.
69
71
 
@@ -80,12 +82,46 @@ imported the same way any pack is, so the measured path is the real one.
80
82
  | `--history-turns N` | `10` | prior messages for the `multi_turn` scenario |
81
83
  | `--output-tokens N` | `48` | tokens the stub streams per turn |
82
84
  | `--json` | off | emit results as JSON (for regression gating) |
85
+ | `--ruby-llm` | off | run the real RubyLLM Chat with an offline provider |
83
86
 
84
87
  `--json` prints the engine version, Ruby/YJIT status, the full config, and every
85
88
  metric — a stable shape to diff across commits.
86
89
 
90
+ ## Offline RubyLLM benchmark
91
+
92
+ ```bash
93
+ bundle exec ruby scripts/bench.rb --ruby-llm --json
94
+ ```
95
+
96
+ This opt-in mode measures the engine **plus RubyLLM's real Chat and tool loop**.
97
+ Only the provider is synthetic: it yields deterministic chunks without HTTP,
98
+ credentials, or a model service. The normal executor creates the Chat, assembles
99
+ its history and tools, and registers the production callbacks. The `tool_call`
100
+ scenario executes two sequential lookup rounds; `greeting` streams an answer and
101
+ `multi_turn` seeds the requested history before streaming.
102
+
103
+ JSON includes the RubyLLM version, `mode`, and per-scenario `ruby_llm_activity`
104
+ counts for provider calls, executed tools, history messages, and streamed chunks.
105
+ Activity counts include warmup; allocation measurements exclude warmup and cover
106
+ both measured passes. Compare matching modes and settings across versions; the
107
+ default stub mode keeps its existing one-tool scenario. Neither mode measures
108
+ HTTP serialization, network latency, or actual model generation.
109
+
110
+ The offline provider supplies request `model_info`, as native RubyLLM 2 usage
111
+ tracking does. Omitting it adds spurious catalog lookups during cost accounting.
112
+ For an isolated, self-checking reproduction without Insika, run
113
+ `ruby scripts/rubyllm_profile.rb 2.0.0 --profile` and repeat with
114
+ `--request-context`. Use `1.16.0` for the old-gem comparison, outside Bundler.
115
+ Method timings are inclusive and must not be summed; use runs without `--profile`
116
+ for latency comparisons. This diagnostic still omits native HTTP processing.
117
+
87
118
  ## Reference numbers
88
119
 
120
+ For the five-run main versus RubyLLM 2 migration comparison repeated on
121
+ 2026-09-24, see [current candidate results](RUBYLLM_2_MIGRATION.md#current-candidate-2026-09-24).
122
+ It uses this harness, plus the API-equivalent baseline harness, without YJIT.
123
+ The older YJIT reference below is not a migration acceptance result.
124
+
89
125
  A reference run. **The absolute milliseconds are machine-specific** — reproduce
90
126
  them on your own hardware with the command below; what travels across machines is
91
127
  the shape (sub-millisecond overhead, flat p95, thousands of turns/s per process).
@@ -109,6 +145,13 @@ work. The rest of any real turn's latency is the provider.
109
145
  ## Publication rule
110
146
 
111
147
  **Any public claim about the engine's performance must reference this suite.**
112
- Numbers produced against a specific provider, deployment, or competitor are not
113
- publishable — they are neither neutral nor reproducible. If a claim cannot be
114
- reproduced by running `scripts/bench.rb`, it does not go in public materials.
148
+ Performance numbers produced against a specific provider, deployment, or competitor
149
+ are not publishable — they are neither neutral nor reproducible. If a performance
150
+ claim cannot be reproduced by running `scripts/bench.rb`, it does not go in public
151
+ materials.
152
+
153
+ The rule is about **performance**, which is what this suite measures. Claims about
154
+ what an agent *gets right* live in a different suite with a different contract: the
155
+ [cross-harness bench](CROSS-HARNESS-BENCH.md) needs a provider key, because a
156
+ commerce conversation cannot be stubbed, and it names other harnesses. It carries its
157
+ own publication rule, which is stricter about the rows it did not write.