insika 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +361 -0
  3. data/LICENSE +21 -0
  4. data/README.md +136 -2
  5. data/bin/insika +366 -0
  6. data/docs/AGENTS.md +618 -0
  7. data/docs/ARCHITECTURE.md +333 -0
  8. data/docs/BENCHMARK.md +114 -0
  9. data/docs/CHANNELS.md +453 -0
  10. data/docs/CONTEXT.md +117 -0
  11. data/docs/DEPLOY.md +354 -0
  12. data/docs/EMBEDDING.md +198 -0
  13. data/docs/EVALS.md +273 -0
  14. data/docs/LOADTEST.md +232 -0
  15. data/docs/OBSERVABILITY.md +374 -0
  16. data/docs/PLUGINS.md +211 -0
  17. data/docs/REFINEMENT.md +477 -0
  18. data/docs/RELEASING.md +70 -0
  19. data/docs/RUNNING-LOCAL.md +153 -0
  20. data/docs/SANDBOX.md +114 -0
  21. data/docs/SECURITY.md +375 -0
  22. data/docs/SKILLS.md +284 -0
  23. data/docs/TOOLS.md +302 -0
  24. data/docs/WHY.md +137 -0
  25. data/docs/WORKFLOWS.md +225 -0
  26. data/docs/build.md +14 -0
  27. data/docs/index.md +68 -0
  28. data/docs/onboarding/start.md +126 -0
  29. data/docs/operate.md +12 -0
  30. data/docs/ship.md +10 -0
  31. data/docs/understand.md +10 -0
  32. data/lib/insika/agent_file_store.rb +125 -0
  33. data/lib/insika/agent_profile.rb +255 -0
  34. data/lib/insika/alert_dispatcher.rb +139 -0
  35. data/lib/insika/allowlist.rb +28 -0
  36. data/lib/insika/baseline_store.rb +74 -0
  37. data/lib/insika/budget_ledger.rb +135 -0
  38. data/lib/insika/capability/resolved_tool.rb +34 -0
  39. data/lib/insika/capability_registry.rb +112 -0
  40. data/lib/insika/channel_delivery.rb +153 -0
  41. data/lib/insika/channel_registry.rb +30 -0
  42. data/lib/insika/channels/relay.rb +178 -0
  43. data/lib/insika/channels/web/widget.js +283 -0
  44. data/lib/insika/channels/web.rb +211 -0
  45. data/lib/insika/channels/webhook.rb +58 -0
  46. data/lib/insika/chat_builder.rb +303 -0
  47. data/lib/insika/checkpoint.rb +13 -0
  48. data/lib/insika/checkpoint_store.rb +153 -0
  49. data/lib/insika/circuit_state.rb +114 -0
  50. data/lib/insika/coercion.rb +58 -0
  51. data/lib/insika/command.rb +32 -0
  52. data/lib/insika/command_bus.rb +39 -0
  53. data/lib/insika/commands/agent_payload.rb +43 -0
  54. data/lib/insika/commands/approve_action.rb +46 -0
  55. data/lib/insika/commands/cancel_task.rb +33 -0
  56. data/lib/insika/commands/create_agent.rb +54 -0
  57. data/lib/insika/commands/create_session.rb +67 -0
  58. data/lib/insika/commands/delete_agent.rb +33 -0
  59. data/lib/insika/commands/delete_agent_file.rb +50 -0
  60. data/lib/insika/commands/delete_data_tool.rb +33 -0
  61. data/lib/insika/commands/delete_llm_provider.rb +36 -0
  62. data/lib/insika/commands/delete_mcp.rb +30 -0
  63. data/lib/insika/commands/delete_skill.rb +43 -0
  64. data/lib/insika/commands/delete_system_file.rb +29 -0
  65. data/lib/insika/commands/gate_refinement.rb +245 -0
  66. data/lib/insika/commands/import_mcp_tools.rb +48 -0
  67. data/lib/insika/commands/import_tools.rb +81 -0
  68. data/lib/insika/commands/issue_tenant_token.rb +41 -0
  69. data/lib/insika/commands/memory_add_note.rb +32 -0
  70. data/lib/insika/commands/memory_forget_fact.rb +32 -0
  71. data/lib/insika/commands/memory_put_fact.rb +35 -0
  72. data/lib/insika/commands/pause_task.rb +29 -0
  73. data/lib/insika/commands/resolve_refinement.rb +126 -0
  74. data/lib/insika/commands/restore_agent_file.rb +36 -0
  75. data/lib/insika/commands/restore_data_tool.rb +34 -0
  76. data/lib/insika/commands/restore_system_file.rb +31 -0
  77. data/lib/insika/commands/resume_task.rb +85 -0
  78. data/lib/insika/commands/revoke_token.rb +39 -0
  79. data/lib/insika/commands/rotate_tenant_token.rb +43 -0
  80. data/lib/insika/commands/run_refinement.rb +133 -0
  81. data/lib/insika/commands/send_message.rb +150 -0
  82. data/lib/insika/commands/set_agent_tools.rb +39 -0
  83. data/lib/insika/commands/set_skill_agents.rb +112 -0
  84. data/lib/insika/commands/trigger_workflow.rb +80 -0
  85. data/lib/insika/commands/update_agent.rb +49 -0
  86. data/lib/insika/commands/update_settings.rb +33 -0
  87. data/lib/insika/commands/upsert_llm_provider.rb +34 -0
  88. data/lib/insika/commands/upsert_mcp.rb +32 -0
  89. data/lib/insika/commands/write_agent_file.rb +57 -0
  90. data/lib/insika/commands/write_data_tool.rb +43 -0
  91. data/lib/insika/commands/write_golden.rb +58 -0
  92. data/lib/insika/commands/write_skill.rb +60 -0
  93. data/lib/insika/commands/write_system_file.rb +31 -0
  94. data/lib/insika/config_store.rb +89 -0
  95. data/lib/insika/context/builder.rb +166 -0
  96. data/lib/insika/context/catalog_provider.rb +23 -0
  97. data/lib/insika/context/fragment.rb +43 -0
  98. data/lib/insika/context/priority.rb +30 -0
  99. data/lib/insika/context/provider.rb +19 -0
  100. data/lib/insika/context/providers/memory.rb +60 -0
  101. data/lib/insika/context/providers/prompt.rb +105 -0
  102. data/lib/insika/context/providers/request.rb +32 -0
  103. data/lib/insika/context/providers/session.rb +123 -0
  104. data/lib/insika/context/providers/skill.rb +24 -0
  105. data/lib/insika/context/providers/skill_trigger.rb +128 -0
  106. data/lib/insika/context/providers/tool_search.rb +20 -0
  107. data/lib/insika/context_trace_store.rb +92 -0
  108. data/lib/insika/delegation_store.rb +153 -0
  109. data/lib/insika/doctor.rb +539 -0
  110. data/lib/insika/dsl/definition.rb +55 -0
  111. data/lib/insika/dsl/runtime.rb +382 -0
  112. data/lib/insika/dsl/server_boot.rb +98 -0
  113. data/lib/insika/dsl/system.rb +93 -0
  114. data/lib/insika/dsl/workflow_adapter.rb +59 -0
  115. data/lib/insika/dsl.rb +364 -0
  116. data/lib/insika/edge_limiter.rb +268 -0
  117. data/lib/insika/egress_guard.rb +75 -0
  118. data/lib/insika/env_schema.rb +249 -0
  119. data/lib/insika/errors.rb +201 -0
  120. data/lib/insika/evals/assertions.rb +247 -0
  121. data/lib/insika/evals/baseline.rb +69 -0
  122. data/lib/insika/evals/golden.rb +172 -0
  123. data/lib/insika/evals/judge.rb +225 -0
  124. data/lib/insika/evals/pairwise.rb +178 -0
  125. data/lib/insika/evals/report.rb +115 -0
  126. data/lib/insika/evals/runner.rb +141 -0
  127. data/lib/insika/evals/transport.rb +178 -0
  128. data/lib/insika/event.rb +18 -0
  129. data/lib/insika/event_stream.rb +132 -0
  130. data/lib/insika/executor.rb +1995 -0
  131. data/lib/insika/frontmatter.rb +42 -0
  132. data/lib/insika/golden_store.rb +145 -0
  133. data/lib/insika/hooks.rb +48 -0
  134. data/lib/insika/http_client.rb +63 -0
  135. data/lib/insika/inbound_log.rb +84 -0
  136. data/lib/insika/llm_configurator.rb +99 -0
  137. data/lib/insika/llm_provider_store.rb +83 -0
  138. data/lib/insika/loop_detector.rb +143 -0
  139. data/lib/insika/mcp_http_client.rb +67 -0
  140. data/lib/insika/mcp_store.rb +115 -0
  141. data/lib/insika/mcp_tool_ingestor.rb +143 -0
  142. data/lib/insika/memory_store.rb +93 -0
  143. data/lib/insika/message_origin.rb +76 -0
  144. data/lib/insika/middleware.rb +36 -0
  145. data/lib/insika/model_policy.rb +52 -0
  146. data/lib/insika/model_resolver.rb +176 -0
  147. data/lib/insika/model_selection.rb +115 -0
  148. data/lib/insika/onboarding.rb +208 -0
  149. data/lib/insika/outbox_store.rb +166 -0
  150. data/lib/insika/overlay_tool_registry.rb +102 -0
  151. data/lib/insika/pack.rb +102 -0
  152. data/lib/insika/pack_importer.rb +123 -0
  153. data/lib/insika/pending_action_store.rb +120 -0
  154. data/lib/insika/plugin/loader.rb +356 -0
  155. data/lib/insika/plugin.rb +35 -0
  156. data/lib/insika/policy/engine.rb +83 -0
  157. data/lib/insika/policy/policy.rb +120 -0
  158. data/lib/insika/policy_registry.rb +23 -0
  159. data/lib/insika/profile_source.rb +143 -0
  160. data/lib/insika/prompt_catalog.rb +61 -0
  161. data/lib/insika/provider_error_classifier.rb +160 -0
  162. data/lib/insika/queue_policy.rb +167 -0
  163. data/lib/insika/recovery.rb +168 -0
  164. data/lib/insika/refinement/candidate.rb +159 -0
  165. data/lib/insika/refinement/evidence_collector.rb +371 -0
  166. data/lib/insika/refinement/gate.rb +234 -0
  167. data/lib/insika/refinement/panel.rb +222 -0
  168. data/lib/insika/refinement/proposer.rb +262 -0
  169. data/lib/insika/refinement_store.rb +295 -0
  170. data/lib/insika/registry.rb +59 -0
  171. data/lib/insika/reliability.rb +185 -0
  172. data/lib/insika/safety/config.rb +109 -0
  173. data/lib/insika/safety/detectors.rb +176 -0
  174. data/lib/insika/safety/factory.rb +102 -0
  175. data/lib/insika/safety/input_guardrail.rb +102 -0
  176. data/lib/insika/safety/moderator.rb +94 -0
  177. data/lib/insika/safety/output_filter.rb +79 -0
  178. data/lib/insika/safety/output_validator.rb +101 -0
  179. data/lib/insika/safety/safe_responses.rb +47 -0
  180. data/lib/insika/sandbox/boundary.rb +93 -0
  181. data/lib/insika/sandbox/docker.rb +74 -0
  182. data/lib/insika/sandbox/local.rb +33 -0
  183. data/lib/insika/sandbox/runner.rb +80 -0
  184. data/lib/insika/sandbox.rb +85 -0
  185. data/lib/insika/schema_guard.rb +147 -0
  186. data/lib/insika/secret_masking.rb +34 -0
  187. data/lib/insika/server/a2a/agent_card.rb +27 -0
  188. data/lib/insika/server/a2a/app.rb +112 -0
  189. data/lib/insika/server/a2a/client.rb +101 -0
  190. data/lib/insika/server/a2a/errors.rb +32 -0
  191. data/lib/insika/server/a2a/http.rb +42 -0
  192. data/lib/insika/server/a2a/message.rb +27 -0
  193. data/lib/insika/server/a2a/protocol.rb +45 -0
  194. data/lib/insika/server/a2a/remotes.rb +25 -0
  195. data/lib/insika/server/a2a/task_projection.rb +40 -0
  196. data/lib/insika/server/app.rb +1022 -0
  197. data/lib/insika/server/boot.rb +119 -0
  198. data/lib/insika/server/rack_app.rb +118 -0
  199. data/lib/insika/server/responses.rb +165 -0
  200. data/lib/insika/server/sse_body.rb +96 -0
  201. data/lib/insika/server/tenant_auth.rb +61 -0
  202. data/lib/insika/session_actor.rb +162 -0
  203. data/lib/insika/session_store.rb +143 -0
  204. data/lib/insika/settings_store.rb +154 -0
  205. data/lib/insika/shutdown.rb +125 -0
  206. data/lib/insika/skill_catalog.rb +220 -0
  207. data/lib/insika/skill_store.rb +127 -0
  208. data/lib/insika/steer_injector.rb +110 -0
  209. data/lib/insika/store.rb +52 -0
  210. data/lib/insika/stores/memory.rb +123 -0
  211. data/lib/insika/stores/sqlite.rb +183 -0
  212. data/lib/insika/studio/app.rb +1693 -0
  213. data/lib/insika/studio/assets/dist/application.css +1 -0
  214. data/lib/insika/studio/assets/dist/application.js +70 -0
  215. data/lib/insika/studio/forms.rb +335 -0
  216. data/lib/insika/studio/nav_icons.rb +31 -0
  217. data/lib/insika/studio/views/_message.erb +44 -0
  218. data/lib/insika/studio/views/agent_detail.erb +285 -0
  219. data/lib/insika/studio/views/agents.erb +63 -0
  220. data/lib/insika/studio/views/approvals.erb +41 -0
  221. data/lib/insika/studio/views/chats.erb +34 -0
  222. data/lib/insika/studio/views/evals.erb +83 -0
  223. data/lib/insika/studio/views/home.erb +72 -0
  224. data/lib/insika/studio/views/layout.erb +94 -0
  225. data/lib/insika/studio/views/login.erb +17 -0
  226. data/lib/insika/studio/views/mcp.erb +91 -0
  227. data/lib/insika/studio/views/not_found.erb +5 -0
  228. data/lib/insika/studio/views/playground.erb +47 -0
  229. data/lib/insika/studio/views/refinement.erb +234 -0
  230. data/lib/insika/studio/views/session.erb +137 -0
  231. data/lib/insika/studio/views/settings.erb +168 -0
  232. data/lib/insika/studio/views/skills.erb +141 -0
  233. data/lib/insika/studio/views/system_files.erb +65 -0
  234. data/lib/insika/studio/views/task.erb +105 -0
  235. data/lib/insika/studio/views/tasks.erb +33 -0
  236. data/lib/insika/studio/views/tool_edit.erb +107 -0
  237. data/lib/insika/studio/views/tools.erb +89 -0
  238. data/lib/insika/subagent_graph.rb +96 -0
  239. data/lib/insika/system_file_store.rb +96 -0
  240. data/lib/insika/task_actor.rb +128 -0
  241. data/lib/insika/task_store.rb +250 -0
  242. data/lib/insika/telemetry/pricing.rb +104 -0
  243. data/lib/insika/telemetry/recorder.rb +228 -0
  244. data/lib/insika/telemetry.rb +127 -0
  245. data/lib/insika/testing/store_contract.rb +270 -0
  246. data/lib/insika/tick.rb +122 -0
  247. data/lib/insika/token_estimator.rb +16 -0
  248. data/lib/insika/token_store.rb +168 -0
  249. data/lib/insika/tool_assembly.rb +140 -0
  250. data/lib/insika/tool_catalog.rb +89 -0
  251. data/lib/insika/tool_definition.rb +518 -0
  252. data/lib/insika/tool_envelope.rb +140 -0
  253. data/lib/insika/tool_manifest.rb +218 -0
  254. data/lib/insika/tool_output_compressor.rb +100 -0
  255. data/lib/insika/tool_registry.rb +21 -0
  256. data/lib/insika/tool_store.rb +135 -0
  257. data/lib/insika/tool_trace_store.rb +92 -0
  258. data/lib/insika/tools/a2a_remote.rb +48 -0
  259. data/lib/insika/tools/agent_enum.rb +68 -0
  260. data/lib/insika/tools/concurrency.rb +54 -0
  261. data/lib/insika/tools/data_defined_tool.rb +219 -0
  262. data/lib/insika/tools/load_skill.rb +99 -0
  263. data/lib/insika/tools/remember.rb +53 -0
  264. data/lib/insika/tools/stuck_signal.rb +44 -0
  265. data/lib/insika/tools/subagent.rb +75 -0
  266. data/lib/insika/tools/subagents.rb +77 -0
  267. data/lib/insika/tools/tool_search.rb +94 -0
  268. data/lib/insika/turn_output.rb +139 -0
  269. data/lib/insika/turn_state.rb +162 -0
  270. data/lib/insika/turn_timing.rb +56 -0
  271. data/lib/insika/usage_ledger.rb +47 -0
  272. data/lib/insika/version.rb +3 -1
  273. data/lib/insika/wiring/graph.rb +249 -0
  274. data/lib/insika/workflow.rb +185 -0
  275. data/lib/insika/workflow_registry.rb +33 -0
  276. data/lib/insika.rb +220 -4
  277. metadata +412 -8
@@ -0,0 +1,477 @@
1
+ ---
2
+ title: Refinement
3
+ parent: Operate & prove it
4
+ nav_order: 5
5
+ permalink: /refinement/
6
+ ---
7
+
8
+ # Refinement
9
+
10
+ An agent in production teaches you something every hour, and by default none of it
11
+ comes back. The engine already records the evidence — every tool call with its
12
+ arguments and result, every transcript, every failed turn — and nobody reads it.
13
+ So the same wrong answer is served to the next customer until a human happens to
14
+ look.
15
+
16
+ Refinement is the loop that closes that gap. It reads a window of an agent's own
17
+ traffic and reports **what broke, how often, and in which conversations**.
18
+
19
+ The report is the default and it is the whole feature until you turn on more: it
20
+ calls no model and it changes nothing about your agent — no prompt is rewritten, no
21
+ tool is touched. That is deliberate. A report you can read in thirty seconds is
22
+ worth more than an automatic edit you cannot verify, and it is the honest way to
23
+ find out whether the evidence in your own deployment is good enough to act on.
24
+
25
+ Opt in (`mode: propose`) and the loop goes one step further: a model proposes a
26
+ small, anchored edit to the instruction files you listed, the edit is scored by
27
+ **running** the agent's test cases with it applied, and a human approves it before
28
+ it reaches anyone. Every part of that is below, including what it cannot catch.
29
+
30
+ ## Running one
31
+
32
+ From the CLI, against the same database the engine uses:
33
+
34
+ ```bash
35
+ INSIKA_DB=insika.db bin/insika refine --agent bia
36
+ ```
37
+
38
+ ```text
39
+ bia — completed (last 200 session(s))
40
+ tool_error ×24 shipping_quote failed: cep is required
41
+ sessions: 9f2c1a04…, 4b7e5590…, c1d0aa31…
42
+ repetition ×7 customer repeated themselves
43
+ quero saber o frete, quanto fica pro meu endereço
44
+ sessions: 9f2c1a04…, 77bb0e12…
45
+ safe_reply ×3 a canned safe reply was served instead of an answer
46
+ sessions: 2a55f0c9…
47
+ tool_unused ×1 search_voucher was never called in this window
48
+ ```
49
+
50
+ Or from **Studio → Refinement**: pick the agent, press Run, and the same report
51
+ renders with each session id linking to its transcript. Every finding is a claim
52
+ you can go and check.
53
+
54
+ There is no scheduler in the engine, by design. A run is one command
55
+ (`run_refinement`), and the CLI and the button are the two ways to fire it. If you
56
+ want it on a timer, point your own cron at the authenticated route the button uses
57
+ — that keeps the engine free of a background ticker and of the single-node
58
+ assumption one would bring.
59
+
60
+ ```
61
+ insika refine ─┐
62
+ Studio button ─┼─▶ run_refinement ─▶ read the window ─▶ ranked report (stored)
63
+ your cron ────┘ (tasks, transcripts, tool traces)
64
+ ```
65
+
66
+ ## The window
67
+
68
+ By default, a run is **incremental**: it reads everything since the previous run
69
+ for that agent, so running it twice in a row is quiet. The first run — or any run
70
+ with `--full` — reads the configured window instead, which defaults to the 200
71
+ most recent conversations.
72
+
73
+ | Flag | Meaning |
74
+ |---|---|
75
+ | `--last-sessions N` | the N most recent conversations, however many turns those took |
76
+ | `--since ISO8601` | only turns from that instant on |
77
+ | `--full` | ignore the previous run and use the configured window |
78
+ | `--exclude PREFIXES` | drop sessions whose id starts with any of these (e.g. `loadtest-,debug-`) |
79
+ | `--json` | the run record as JSON, for a pipeline |
80
+
81
+ The window is stored on the run, so a report read months later still says what it
82
+ looked at.
83
+
84
+ ## Synthetic traffic
85
+
86
+ If load tests, evals or debug conversations land in the same store as real ones,
87
+ they will dominate the report — on the pilot they outnumbered real conversations
88
+ and buried every genuine finding, including 203 turns failing for a reason that
89
+ only occurs under the load-test profile.
90
+
91
+ `exclude_sessions` (or `--exclude`) drops sessions by id prefix. It defaults to
92
+ **nothing**: a report does not get to decide what counts as real traffic. What it
93
+ dropped is counted on the run and shown next to the findings, so a filtered window
94
+ never reads like a clean deployment.
95
+
96
+ ## What it looks for
97
+
98
+ Each finding is aggregated: forty instances of one broken argument are **one**
99
+ finding with a count of forty, not forty rows. Findings are ranked by count times
100
+ severity, and each carries up to five session ids as provenance — ids only, so the
101
+ record itself holds no conversation content.
102
+
103
+ | Finding | What it means | Read from |
104
+ |---|---|---|
105
+ | `tool_error` | a tool returned an error, grouped by tool and by a normalized error signature | tool traces |
106
+ | `task_failed` | a turn died, grouped by its error | the task's executions |
107
+ | `repetition` | the customer said the same thing twice in a row — the outside view of an instruction the agent is not following | the transcript |
108
+ | `safe_reply` | a canned safe reply reached the customer instead of an answer | the transcript |
109
+ | `tool_unused` | a tool the agent is allowed to use never fired in the whole window | profile vs tool traces |
110
+
111
+ Two notes on honesty, because a report that overstates what it knows is worse than
112
+ no report:
113
+
114
+ - **`repetition` is a heuristic**, not a judgment: token overlap between
115
+ consecutive customer messages, with short messages ignored so a repeated "hi"
116
+ does not count. It calls no model.
117
+ - **`safe_reply` cannot tell you which rule fired.** Guardrail decisions and
118
+ edge-limit hits are emitted as events and never stored, so the canned reply in
119
+ the transcript is their only durable footprint. The finding tells you the agent
120
+ gave up; the guardrail configuration tells you why it might have.
121
+
122
+ Numbers and ids are normalized out of the grouping key, so `product 4711 not found`
123
+ and `product 4712 not found` are recognized as one defect.
124
+
125
+ ## Who wrote a message
126
+
127
+ A `role` says where a message sits in the conversation, not who wrote it, and the two
128
+ come apart constantly: the engine delivers a subagent's result as a `user` turn, a
129
+ guardrail answers as `assistant` with no model involved, a consumer composes context
130
+ blocks into the input, and in an imported transcript a human operator types after a
131
+ handoff. Read without that distinction, the first run over real traffic reported
132
+ **219** "the customer repeated themselves" that were the engine reading its own
133
+ injected fragment back.
134
+
135
+ So a stored message may carry an `origin`:
136
+
137
+ | origin | who |
138
+ |---|---|
139
+ | *(absent)* | the natural producer for the role — a customer for `user`, the model for `assistant` |
140
+ | `customer` | a person, said explicitly |
141
+ | `agent` | the model, said explicitly |
142
+ | `engine` | Insika itself, or a consumer composing on its behalf |
143
+ | `operator` | a **human** on the assistant side (a handoff) — set by whatever imports the transcript |
144
+
145
+ The engine stamps what it truthfully knows: a delegation result it wrote, a guardrail
146
+ reply it produced. A consumer declares its own composed input by sending `"origin"` on
147
+ `POST /v1/responses`. Nothing else changes — a message with no origin reads exactly as
148
+ it did before, so no transcript needs migrating.
149
+
150
+ `repetition` counts only what a customer said and `safe_reply` reads only what the
151
+ engine said, both from this field. A message that declares nothing falls back to the
152
+ old guess (an injected fragment opens with its own tag, `<store_cep_required> …`,
153
+ which no customer types) — that heuristic now runs only on messages that made no
154
+ claim about themselves.
155
+
156
+ ## Privacy
157
+
158
+ A report may quote customer words — that is the point of the `repetition` snippet —
159
+ so every snippet goes through the same redaction a customer-facing turn does
160
+ (`[REDACTED:cpf]` and friends, see [Security](SECURITY.md)). Tool arguments and
161
+ results are never copied into a report at all; only the error signature is. And the
162
+ run record stores session ids, never their contents.
163
+
164
+ That redaction covers what the engine's detectors cover — formatted CPF/CNPJ and
165
+ API secrets. It is not a general PII scrubber: a phone number written into a chat
166
+ can survive into a snippet. Treat the Refinement page as what it is — an operator
167
+ surface behind the Studio login, next to the transcripts themselves.
168
+
169
+ ## Configuration
170
+
171
+ Refinement needs no opt-in to report: reading your own traffic writes nothing, so
172
+ an agent with no configuration at all can be run. The optional block on the agent
173
+ sets the defaults:
174
+
175
+ ```ruby
176
+ Insika.agent "bia" do
177
+ model "deepseek-v4-flash"
178
+ refine window: { last_sessions: 200 }, max_findings: 20
179
+ end
180
+ ```
181
+
182
+ | Key | Default | Meaning |
183
+ |---|---|---|
184
+ | `window.last_sessions` | 200 | conversations read when the run is not incremental |
185
+ | `max_findings` | 20 | cap on the report |
186
+ | `exclude_sessions` | none | session-id prefixes to drop |
187
+ | `mode` | `"report"` | `report` reads and writes nothing. `propose` allows a gated, human-approved edit (below). `auto_apply` lets a gate-passing edit land unattended — off by default, and read [what it costs you](#applying-without-a-human) first. A mode the engine does not know is refused, never silently downgraded |
188
+ | `files` | none | the ONLY files a proposal may edit. Empty means report-only |
189
+ | `proposer` | the platform `utility_model` | which model writes the candidate (`"deepseek/deepseek-v4-flash"` or a bare model name). Neither set means no proposal — the engine never picks a model to spend your budget on |
190
+ | `proposers` | falls back to `proposer` | a **panel**: several models, each writing its own candidate. `["deepseek/deepseek-v4-flash", {model: "gpt-5-mini", provider: "openai"}]` — either syntax |
191
+ | `budget.tokens` | unlimited | what one run may spend across every proposal and every gate replay |
192
+ | `max_edits` | 3 | edits a single proposal may carry |
193
+ | `auto_apply_max_edits` | 1 | edits an **unattended** apply may carry. A bigger diff waits for a person |
194
+ | `max_bytes` | 1200 | size of one edit's replacement text |
195
+ | `max_total_growth` | 0.15 | how much a proposal may grow a file, as a fraction of its current size |
196
+
197
+ ## Events
198
+
199
+ A run emits two events, both counts and no content, so any subscriber (including
200
+ the OpenTelemetry bridge — see [Observability](OBSERVABILITY.md)) can watch it:
201
+
202
+ | Event | Data |
203
+ |---|---|
204
+ | `:refinement_started` | agent, run id, window |
205
+ | `:refinement_report` | agent, run id, status, findings, sessions, turns |
206
+ | `:refinement_proposed` | agent, run id, candidates, proposers, edits |
207
+ | `:refinement_gated` | agent, run id, passed, reason, cases, passed_cases, regressions, candidates, tokens |
208
+ | `:refinement_applied` | agent, run id, by, files, edits |
209
+ | `:refinement_rejected` | agent, run id, by |
210
+ | `:refinement_auto_apply_skipped` | agent, run id, edits, max_edits |
211
+
212
+ File **names** appear on the applied event, because an operator needs to know what
213
+ changed. File **contents** never do.
214
+
215
+ ## Changing the agent: the gate
216
+
217
+ A report tells you what broke. Changing the prompt because of it is a separate,
218
+ opt-in step, and the whole design is in one sentence: **a proposed edit is scored by
219
+ running it, and a human approves it before it reaches anyone.**
220
+
221
+ ```
222
+ proposal ──▶ clone the agent ──▶ apply the edits to the CLONE ──▶ replay the golden
223
+ set ──▶ compare to the accepted baseline ──▶ a human approves ──▶ write
224
+ ```
225
+
226
+ Nothing here asks a model whether an edit looks good. That measures nothing. What
227
+ the gate measures is whether the agent still passes the cases it was passing, on
228
+ real turns, with its real tools and guardrails.
229
+
230
+ ### What a proposal looks like
231
+
232
+ Data, not a rewritten file:
233
+
234
+ ```jsonc
235
+ {
236
+ "rationale": "Two findings share a cause: TOOLS.md never says the CEP is required.",
237
+ "edits": [{
238
+ "file": "TOOLS.md", // must be in `files`
239
+ "op": "replace", // replace | append
240
+ "anchor": "## shipping_quote", // a label for the reviewer
241
+ "before": "Use shipping_quote to quote freight.", // must still match, exactly and once
242
+ "after": "Use shipping_quote to quote freight. Always ask for the CEP first.",
243
+ "addresses": ["tool_error:shipping_quote"]
244
+ }]
245
+ }
246
+ ```
247
+
248
+ Anchored and small is not a style preference. It makes the diff a five-second
249
+ decision instead of a code review, it makes the gate's result attributable to an
250
+ edit you can point at, and it makes staleness detectable: if `before` no longer
251
+ matches the file, the edit is dropped rather than applied by fuzzy match — which is
252
+ how a loop like this would otherwise silently overwrite something you wrote.
253
+
254
+ An edit that breaks a bound is dropped **with a reason** and the rest of the
255
+ proposal still goes to the gate. A proposal whose every edit dropped is refused
256
+ before anything runs.
257
+
258
+ ### Who writes it
259
+
260
+ You can hand a candidate to the API yourself. Or press **Propose a fix** on a
261
+ finished report and the model named by `proposer` writes one, from the findings and
262
+ the current text of the allowlisted files.
263
+
264
+ That model is the weakest link in the loop, and it is built to be. It is shown the
265
+ evidence and the files it may edit; it produces data that is then bounded (allowlist,
266
+ size, growth, an anchor that must still match) and **scored by replaying your golden
267
+ set**. A hallucinated rationale, a misread finding, an invented anchor — the worst
268
+ outcome of each is a candidate that gets dropped or fails to move a score, and never
269
+ reaches a customer. Nothing it says is trusted; it is measured.
270
+
271
+ Three things follow from that, and they are worth knowing before you press the
272
+ button:
273
+
274
+ - **It only sees the files on your allowlist.** Not the rest of the prompt, not your
275
+ guardrails, not your tools. A model that can read a file it cannot edit proposes
276
+ edits to it, which drop, which spends your attention on rejects.
277
+ - **It only sees findings.** A run with none refuses to propose rather than inventing
278
+ an improvement to a prompt that is working.
279
+ - **It costs money twice** — once to write the candidate, once for the gate's replay,
280
+ which is a real conversation per case. So a proposal is a deliberate press, never a
281
+ timer, and the engine refuses if no `proposer` is configured rather than picking a
282
+ model for you.
283
+
284
+ The report and the proposal run in the same place they always did: `insika refine`
285
+ and Studio → Refinement. The proposal is Studio-only, because the gate's replay goes
286
+ through the deployment's own `/v1/responses` — the CLI runs without booting the app,
287
+ which is what makes it safe against a live volume, and it is not going to start a
288
+ server to grade an edit.
289
+
290
+ **What the proposals actually look like**, from running this against a real
291
+ production-shaped agent (a 22 KB persona, an 11 KB tool guide, seventeen findings
292
+ from its own traffic): most were edits a human would have made — reuse the search
293
+ result you already have instead of searching again, say one honest sentence when a
294
+ tool fails instead of retrying it. One was not, and it is the failure mode to know
295
+ about: **an infrastructure finding invites prose that cannot work.** Shown tool
296
+ errors that were really a blocked destination and a refused connection, the model
297
+ proposed instructing the agent to "always use https" — advice about something the
298
+ agent does not control and cannot obey. Naming that trap in the proposer's own
299
+ instructions removed it, and the model now says out loud which findings it is
300
+ declining to address. It will not catch every case: when you review a proposal, the
301
+ first question worth asking is whether the finding it addresses is behaviour at all.
302
+
303
+ ### More than one proposer
304
+
305
+ `proposers` asks several models the same question and gates every answer:
306
+
307
+ ```ruby
308
+ refine mode: "propose", files: %w[TOOLS.md],
309
+ proposers: ["deepseek/deepseek-v4-flash", "gpt-5-mini"],
310
+ budget: { tokens: 200_000 }
311
+ ```
312
+
313
+ They are **independent, not consensus-seeking**. Each is shown the same findings and
314
+ the same files and writes its own candidate; the gate then scores each one and you
315
+ are shown the best survivor, with the others listed under it. Convergence only ever
316
+ breaks a tie: two models agreeing on wording is weak evidence, and a golden case
317
+ passing is strong evidence. Ranking is highest score, then the smaller diff, then how
318
+ many models converged.
319
+
320
+ Two models that write the *identical* edit are gated once, not twice — the agreement
321
+ is recorded and the replay is not paid for again. A model that answers prose, times
322
+ out or 500s takes itself out of the panel and the rest proceeds; all of them failing
323
+ is an error, not a silent empty result. The panel runs concurrently and is capped at
324
+ the subagent fan-out (8, `INSIKA_SUBAGENT_FANOUT_CAP`).
325
+
326
+ A panel of one is exactly what `proposer` already did, which is why nothing changes
327
+ for an agent that names a single model.
328
+
329
+ ### What a run may spend
330
+
331
+ A panel of 3 over a 7-case golden set is 3 model calls plus **21 replayed
332
+ conversations**, each a real turn with real tools. That is the honest objection to
333
+ this whole feature, and `budget.tokens` is the answer to it: a ceiling checked before
334
+ each expensive step, never in the middle of one. A candidate the run could not afford
335
+ is recorded as "not gated — the budget was spent", never dropped in silence, and the
336
+ run's cost is on the record where you can see whether the loop earns its keep.
337
+
338
+ Two things to know about the number.
339
+
340
+ **It counts the prompt cache.** A turn on a 27 KB pack reports `total_tokens: 88` with
341
+ `cached_tokens: 26624` — the engine's `total_tokens` is input + output and deliberately
342
+ excludes the cached prefix. A budget built on that alone would let a run send hundreds
343
+ of times what its ceiling said, so the budget bills `total + cached` and records the
344
+ cached share separately. On a real panel run against the pilot: **382,325 tokens spent,
345
+ 362,752 of them cached** — 95%. Cached tokens are cheaper than fresh ones; they are not
346
+ free, and a ceiling has to see them.
347
+
348
+ **And when a provider reports nothing**, that leg is tallied as *unmetered* rather than
349
+ as zero, because a budget that quietly reads unmetered spend as free stops being a
350
+ budget. If your provider is silent, the bounds that still hold are structural: the
351
+ fan-out cap on the panel, `max_edits`, and the gate's own refusals.
352
+
353
+ ### What the gate needs
354
+
355
+ Two things, and it refuses without either:
356
+
357
+ - **Golden cases for the agent.** No cases, no gate, no writes. Declaring them is
358
+ the price of admission to automated editing — and the cheapest thing you can do
359
+ to make this safe. See [Evals](EVALS.md).
360
+ - **A recorded baseline** — the accepted state of those cases:
361
+
362
+ ```bash
363
+ insika evals:import # the corpus into the store
364
+ insika evals:baseline import # the accepted state, per agent
365
+ insika evals:baseline show
366
+ ```
367
+
368
+ Without one, "did anything regress?" has no answer, and a gate that answered
369
+ "nothing regressed" would be reporting that it did not look. So it refuses.
370
+
371
+ It also refuses a baseline in which **nothing passes**. A regression is measured
372
+ against a case that *was* passing, so an all-red baseline cannot produce one and
373
+ every candidate — including a harmful one — sails through. If that is where you
374
+ are, the agent needs fixing before it needs refining: get to a green run, record
375
+ it, then gate.
376
+
377
+ - **A judge, if your baseline was recorded with one.** A rubric'd case with no judge
378
+ verdict counts as a pass, so replaying without a judge against a judged baseline
379
+ does not measure less — it measures backwards, and every candidate reads as an
380
+ improvement. Measured: gating the pilot with no judge configured reported **6/6, no
381
+ regression** against a baseline the same corpus had just scored **2/6**. So the gate
382
+ refuses that combination. Configure the panel in Studio → Settings → Evals (or
383
+ `settings["evals"]["judges"]`), or re-record the baseline without a judge — then both
384
+ sides are equally deterministic, which is weak but not inverted.
385
+
386
+ With the judge on, the same two candidates were **rejected**: both dropped
387
+ `status-pedido` from 1.0 to 0.7, one also dropped `saudacao`. That is the gate doing
388
+ its job, on edits the broken version had waved through.
389
+
390
+ The clone is a throwaway agent (`<agent>-cand-<run>`) with the same profile, tools
391
+ and guardrails, and it is deleted afterwards — including when the replay fails.
392
+ **Any** regression disqualifies the candidate. A case that was already failing does
393
+ not: refinement exists to fix those.
394
+
395
+ ### Approving
396
+
397
+ A candidate that passes the gate parks the run at `awaiting_approval` and shows up
398
+ on the Refinement page with the diff, what it claims to address, and its score. You
399
+ approve or reject; nothing applies itself.
400
+
401
+ Approving writes each edit through the agent's file store, which versions the
402
+ previous content — so **rollback is the Restore button that was already there**, in
403
+ the file's History. There is no separate undo to learn.
404
+
405
+ Between the gate and your approval, someone may have edited the same file by hand.
406
+ The apply re-checks every `before` against the file as it is now and refuses the
407
+ whole proposal if anything drifted. A partial application would leave a prompt in a
408
+ state nobody reviewed and the gate never scored.
409
+
410
+ ### Applying without a human
411
+
412
+ `mode: "auto_apply"` is the one setting that lets a prompt change while nobody is
413
+ watching. It is off by default and it is deliberately narrow — it needs **all** of:
414
+
415
+ - the mode, set explicitly on that agent;
416
+ - a gate **pass** with zero regressions (a refused candidate is never auto-applied);
417
+ - a diff no larger than `auto_apply_max_edits`, which defaults to **1**.
418
+
419
+ A candidate that passes but is too large is **not rejected** — it waits for a person.
420
+ "Too big to apply unattended" and "wrong" are different verdicts, and collapsing them
421
+ would throw away a proposal the gate already paid to score.
422
+
423
+ An unattended apply goes through the same code an approval does: the same staleness
424
+ re-check, the same versioned write, the same `:refinement_applied` event. So the undo
425
+ is the same one — Restore in the file's History — and the Refinement page shows what
426
+ changed, why, and the link to get there.
427
+
428
+ The honest framing: this trades your review for your golden set. It is worth turning
429
+ on when the cases genuinely cover the behaviour you care about, and it is a bad idea
430
+ before that — see [what the gate cannot catch](#what-the-gate-can-and-cannot-catch),
431
+ which is the list of things auto-apply will happily wave through.
432
+
433
+ ### What the gate can and cannot catch
434
+
435
+ Worth being precise about, because the gate is easy to trust more than it deserves.
436
+ Everything below was measured by running it against a real production-shaped agent,
437
+ not reasoned about.
438
+
439
+ **The gate is only as strong as your golden set.** This is the whole caveat and the
440
+ rest is detail. A regression is "a case that was passing now fails" — so an edit
441
+ that breaks something no case covers passes cleanly. Two shallow cases wave almost
442
+ anything through. If you want the loop to protect a behaviour, there has to be a
443
+ case for that behaviour; that is the price [the gate](#what-the-gate-needs) is charging,
444
+ and it is charged in curation work, not in configuration.
445
+
446
+ Three things it will **not** catch, and two of them are the engine working correctly:
447
+
448
+ - **An edit cannot remove a tool, so the gate will never see one disappear.** Tool
449
+ availability comes from the agent's `tools_allow`, not from prose. An instruction
450
+ like "never call `search_products`" is advice the model routinely overrides; the
451
+ tool is still attached and still gets called. If you want a tool gone, remove it
452
+ from the agent — which refinement cannot do, by design.
453
+ - **PII in a reply is redacted before the gate could grade it.** The output
454
+ guardrail runs on the turn, so a `must_not: [cpf]` case cannot fail because of an
455
+ edit that tells the agent to leak one. The protection is real; it just means this
456
+ is not the layer that measures it.
457
+ - **A small edit in a large prompt may change nothing at all.** A paragraph appended
458
+ to the end of an 11 KB instruction set routinely loses to the rest of it. A gate
459
+ pass on such an edit is honest — nothing changed — but it is not evidence that the
460
+ edit *worked*, and approving it adds prompt with no effect.
461
+
462
+ What it does catch reliably is the class that matters most in practice: an edit that
463
+ changes **what the agent says** in a way one of your cases checks. Formatting,
464
+ tone, how much it asks before acting, whether it follows a policy. That is where
465
+ prompt edits have real leverage, and it is also where they do damage.
466
+
467
+ ## What this is not
468
+
469
+ It has no scheduler: a run happens because a person or a cron asked for one, never
470
+ because a timer inside the engine went off. A proposal is written when you ask for
471
+ one, and — unless you turned on [`auto_apply`](#applying-without-a-human), which is
472
+ off until you do and bounded when you do — applied when you approve it. And it cannot
473
+ touch your guardrails, tools, policies, model pins or limits, and not because a prompt
474
+ tells it not to — there is no code path (see [Security](SECURITY.md)).
475
+
476
+ And if the findings turn out to be noise in your deployment, the correct answer is
477
+ to stop at the report. That is a valid steady state, not a half-finished setup.
data/docs/RELEASING.md ADDED
@@ -0,0 +1,70 @@
1
+ ---
2
+ title: Releasing
3
+ parent: Ship it
4
+ nav_order: 5
5
+ permalink: /releasing/
6
+ ---
7
+
8
+ # Releasing
9
+
10
+ How an `insika` gem release is cut, and how the install is **proven** before the
11
+ push. The rule that matters: a green suite is not a green gem —
12
+ the suite resolves everything by path, so the entire class of packaging failure
13
+ is invisible to it. Do not publish on rspec alone.
14
+
15
+ ## Before anything
16
+
17
+ 1. The suite is green: `bundle exec rspec`.
18
+ 2. `lib/insika/version.rb` carries the version being published.
19
+ 3. Every new `lib/` file is **tracked in git**. The gemspec's `files` come from
20
+ `git ls-files`: an untracked file builds without a warning and the installed
21
+ gem fails at `require` — this is exactly the failure this proof exists to catch.
22
+
23
+ ## Cut the gem
24
+
25
+ ```bash
26
+ gem build insika.gemspec # -> insika-<version>.gem
27
+ ```
28
+
29
+ ## Prove the install — from OUTSIDE the repo
30
+
31
+ Install into a clean gem home and run the four shapes from a directory that is
32
+ not the checkout, with the repo's `lib/` nowhere on the load path:
33
+
34
+ ```bash
35
+ T=$(mktemp -d)
36
+ gem install --install-dir "$T/gemhome" insika-<version>.gem
37
+ cd "$T"
38
+
39
+ # 1. reply in-process
40
+ GEM_HOME="$T/gemhome" GEM_PATH="$T/gemhome" ruby -e '
41
+ require "insika"
42
+ agent = Insika.agent("assistant") { model "deepseek-v4-flash"; provider :deepseek }
43
+ puts agent.reply("hi")' # needs DEEPSEEK_API_KEY
44
+
45
+ # 2. serve — /studio login 200, /v1/responses streams, /start.md 200
46
+ # 3. Insika::Server.rack_app mounted under the host's own router (Rack::URLMap)
47
+ # 4. Insika.embed(backend: Insika::Stores::SQLite.new(path: "e1.db")) { … }.reply
48
+ ```
49
+
50
+ Shapes 2–4 are the ones that fail when a file is missing from the gem (the
51
+ Studio's `views/`, `assets/dist/`, the onboarding docs); run all four.
52
+
53
+ Then the load guard, from the **installed** gem — no test double:
54
+
55
+ ```bash
56
+ GEM_HOME="$T/gemhome" GEM_PATH="$T/gemhome" ruby -e '
57
+ require "insika"
58
+ abort "leak" if %w[RubyLLM Roda Falcon SQLite3 OpenTelemetry].any? { |c| Object.const_defined?(c) }
59
+ puts "clean"'
60
+ ```
61
+
62
+ ## Publish
63
+
64
+ ```bash
65
+ gem push insika-<version>.gem
66
+ ```
67
+
68
+ Publishing is irreversible in practice — a yanked 0.1.0 is a bad first
69
+ impression. The version number is cheap; the name is not. The install proof before
70
+ push, and nothing else.