insika 0.7.0 → 0.9.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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +191 -0
  3. data/README.md +9 -6
  4. data/bin/insika +44 -3
  5. data/docs/AGENTS.md +74 -14
  6. data/docs/API.md +73 -0
  7. data/docs/ARCHITECTURE.md +45 -44
  8. data/docs/ARTIFACTS.md +42 -0
  9. data/docs/CHANNELS.md +19 -2
  10. data/docs/CONTEXT.md +86 -38
  11. data/docs/DEPLOY.md +27 -7
  12. data/docs/EVALS.md +98 -8
  13. data/docs/FACTS.md +4 -0
  14. data/docs/KNOWLEDGE.md +7 -0
  15. data/docs/LOADTEST.md +15 -27
  16. data/docs/MEDIA.md +1 -1
  17. data/docs/OBSERVABILITY.md +48 -4
  18. data/docs/POLICY.md +14 -5
  19. data/docs/RELEASING.md +4 -0
  20. data/docs/RUNNING-LOCAL.md +2 -2
  21. data/docs/SECURITY.md +28 -2
  22. data/docs/SOAK.md +1 -1
  23. data/docs/TOOLS.md +150 -32
  24. data/docs/prompts/ADD-TOOL.md +12 -2
  25. data/docs/prompts/DIAGNOSE-TURN.md +3 -0
  26. data/docs/prompts/GO-LIVE.md +6 -4
  27. data/lib/insika/agent_profile.rb +47 -10
  28. data/lib/insika/channels/web/widget.js +33 -0
  29. data/lib/insika/channels/web.rb +5 -2
  30. data/lib/insika/chat_builder.rb +90 -37
  31. data/lib/insika/commands/agent_payload.rb +1 -1
  32. data/lib/insika/commands/run_distillation.rb +5 -8
  33. data/lib/insika/commands/seed_session.rb +118 -0
  34. data/lib/insika/compaction.rb +196 -0
  35. data/lib/insika/context/builder.rb +35 -11
  36. data/lib/insika/context/fragment.rb +4 -1
  37. data/lib/insika/context/priority.rb +8 -0
  38. data/lib/insika/context/provider.rb +5 -0
  39. data/lib/insika/context/providers/briefing.rb +61 -29
  40. data/lib/insika/context/providers/fence_notice.rb +27 -0
  41. data/lib/insika/context/providers/knowledge.rb +7 -4
  42. data/lib/insika/context/providers/memory.rb +8 -4
  43. data/lib/insika/context/providers/session.rb +50 -10
  44. data/lib/insika/context_trace_store.rb +11 -1
  45. data/lib/insika/doctor.rb +213 -10
  46. data/lib/insika/dsl/runtime.rb +5 -0
  47. data/lib/insika/dsl.rb +6 -0
  48. data/lib/insika/edge_limiter.rb +4 -1
  49. data/lib/insika/env_schema.rb +5 -6
  50. data/lib/insika/errors.rb +1 -0
  51. data/lib/insika/evals/assertions.rb +92 -6
  52. data/lib/insika/evals/golden.rb +91 -2
  53. data/lib/insika/evals/runner.rb +20 -0
  54. data/lib/insika/evals/simulator.rb +11 -2
  55. data/lib/insika/evals/transport.rb +118 -16
  56. data/lib/insika/evidence.rb +79 -12
  57. data/lib/insika/executor.rb +94 -23
  58. data/lib/insika/fence.rb +96 -0
  59. data/lib/insika/golden_store.rb +3 -0
  60. data/lib/insika/loop_detector.rb +5 -34
  61. data/lib/insika/mcp_store.rb +5 -2
  62. data/lib/insika/mcp_tool_registry.rb +8 -1
  63. data/lib/insika/memory_store.rb +12 -0
  64. data/lib/insika/overlay_tool_registry.rb +5 -0
  65. data/lib/insika/prefix_fingerprint.rb +32 -27
  66. data/lib/insika/profile_source.rb +8 -0
  67. data/lib/insika/server/app.rb +43 -1
  68. data/lib/insika/server/rack_app.rb +2 -0
  69. data/lib/insika/server/responses.rb +35 -8
  70. data/lib/insika/session_store.rb +38 -5
  71. data/lib/insika/settings_store.rb +18 -2
  72. data/lib/insika/soak/runner.rb +4 -4
  73. data/lib/insika/spoken_transcript.rb +31 -0
  74. data/lib/insika/studio/app.rb +34 -8
  75. data/lib/insika/studio/forms.rb +29 -3
  76. data/lib/insika/studio/views/_agent_tab_config.erb +5 -1
  77. data/lib/insika/studio/views/session.erb +1 -1
  78. data/lib/insika/studio/views/settings.erb +11 -0
  79. data/lib/insika/studio/views/tool_edit.erb +6 -2
  80. data/lib/insika/telemetry/recorder.rb +61 -1
  81. data/lib/insika/templates/daily-digest/README.md +9 -0
  82. data/lib/insika/templates/research-analyst/agent.rb +10 -0
  83. data/lib/insika/tool_assembly.rb +21 -13
  84. data/lib/insika/tool_batch.rb +67 -0
  85. data/lib/insika/tool_definition.rb +73 -10
  86. data/lib/insika/tool_envelope.rb +102 -2
  87. data/lib/insika/tool_store.rb +9 -4
  88. data/lib/insika/tool_trace_store.rb +1 -1
  89. data/lib/insika/tool_usage_report.rb +172 -0
  90. data/lib/insika/tools/data_defined_tool.rb +1 -0
  91. data/lib/insika/tools/present.rb +122 -0
  92. data/lib/insika/tools/run_persona_eval.rb +6 -1
  93. data/lib/insika/tools/tool_search.rb +4 -2
  94. data/lib/insika/turn_budget.rb +91 -0
  95. data/lib/insika/turn_state.rb +13 -1
  96. data/lib/insika/version.rb +1 -1
  97. data/lib/insika/wiring/graph.rb +7 -0
  98. data/lib/insika/wiring/graph_chat.rb +4 -0
  99. data/lib/insika.rb +11 -0
  100. metadata +10 -1
data/docs/ARCHITECTURE.md CHANGED
@@ -177,64 +177,65 @@ The order is not arbitrary:
177
177
 
178
178
  ## The tool-loop
179
179
 
180
- Stage 6 is the single agent interaction. RubyLLM owns the reason→act→observe loop;
181
- the engine wraps each tool the model may call in a **ToolEnvelope** that enforces
182
- the per-tool timeout, records side-effects for checkpointing, skips
183
- already-completed side-effects on resume, and fires the approval gate.
180
+ Stage 6 is the agent interaction. RubyLLM owns the reason→act→observe loop;
181
+ `ToolEnvelope` adds provenance, approval, concurrency, timeout, evidence processing,
182
+ fencing, traces and side-effect checkpoints around registered tools.
184
183
 
185
- When a step contains several tool calls they are executed **one at a time**, unless
186
- the agent raised `limits[:tool_concurrency]` then the batch runs on the turn's
187
- reactor with at most that many in flight, one fiber per call, and the envelope's
188
- shared semaphore is the cap. A turn with an approval-required tool always runs
189
- serially. See [Tools](TOOLS.md#parallel-tool-calls) for what that changes.
184
+ Calls run serially unless `limits[:tool_concurrency]` permits parallel execution.
185
+ Marked side effects acquire a serial gate before the shared concurrency slot,
186
+ so writes in one session cannot overlap and queued writes leave slots for reads.
187
+ Turns exposing approval-required tools run serially. See
188
+ [Tools](TOOLS.md#parallel-tool-calls) for limits and timeout behavior.
190
189
 
191
190
  ```mermaid
192
191
  flowchart TD
193
- ask[chat.ask -> model] --> dec{tool call?}
192
+ ask[chat.ask model] --> dec{tool call?}
194
193
  dec -->|no| done[final content]
195
- dec -->|yes| env[ToolEnvelope]
196
- env --> appr{approval<br/>required?}
197
- appr -->|yes| suspend[[suspend turn<br/>await operator]]
198
- suspend --> appr
199
- appr -->|no / approved| kind{tool kind}
200
- kind -->|code| ruby[Ruby class<br/>in-process / sandbox]
201
- kind -->|data / MCP| egress[EgressGuard] --> http[(external HTTP)]
202
- ruby --> obs[result -> back to model]
203
- http --> obs
204
- obs --> ask
194
+ dec -->|yes| env[ToolEnvelope: skip completed side effects on resume]
195
+ env --> prov{declared evidence IDs known?}
196
+ prov -->|no| blocked[blocked: provenance]
197
+ blocked --> ask
198
+ prov -->|yes / undeclared| appr[approval gate]
199
+ appr --> gates[side-effect serial gate → shared concurrency slot → timeout]
200
+ gates --> kind{tool kind}
201
+ kind -->|code| ruby[Ruby / sandbox]
202
+ kind -->|HTTP data| http[EgressGuard HTTP]
203
+ kind -->|MCP| mcp[live MCP client]
204
+ kind -->|presentation| cards[select turn or session evidence cards → UI event]
205
+ ruby --> result[evidence reshape → fencing → checkpoint and trace]
206
+ http --> result
207
+ mcp --> result
208
+ cards --> result
209
+ result --> ask
205
210
  ```
206
211
 
207
- A **data tool** is config, not code (see [Tools](TOOLS.md)): its result comes back
208
- to the model exactly like a code tool's, but it went out over HTTP through the
209
- egress guard. A tool exception is caught and returned *to the model* as an error
210
- result it does not crash the turn. Side-effecting tools (POST and friends) are
211
- recorded in the checkpoint so a resume does not re-run them.
212
+ 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
214
+ is checked before a request. Tool errors reach the model as error results so it
215
+ can recover; a provenance refusal reports `blocked` without contacting the backend
216
+ or asking an operator. Completed side effects are recorded for resume.
212
217
 
213
218
  ## Ingesting tools: manifest and MCP
214
219
 
215
- Tools become data in the store through two runtime paths, both hot (no restart):
220
+ Data definitions and MCP instances have separate stores. Both are hot config:
216
221
 
217
222
  ```mermaid
218
223
  flowchart TD
219
- subgraph manifest [Manifest path]
220
- m["POST /v1/tools/manifest"] --> sub["substitute<br/>{{env.*}} / {{secret.*}}"]
221
- sub --> val1[validate each tool]
222
- end
223
- subgraph mcp [MCP path]
224
- srv[(MCP server<br/>HTTP transport)] --> ing[MCP ingestor]
225
- ing --> conv[each tool HTTP data tool<br/>JSON-RPC tools/call]
226
- conv --> val2[validate]
227
- end
228
- val1 --> store[(ToolStore)]
229
- val2 --> store
230
- store --> cat[reload catalog + registry]
231
- cat --> loop[available in the tool-loop]
224
+ manifest[POST /v1/tools/manifest] --> substitute[resolve env / secret placeholders]
225
+ substitute --> validate[validate HTTP or presentation definition]
226
+ validate --> store[(ToolStore)]
227
+ store --> data[DataToolRegistry]
228
+ config[MCP instance configuration] --> mcpstore[(McpStore)]
229
+ mcpstore --> live[McpToolRegistry live server tools]
230
+ data --> catalog[effective registry and catalog]
231
+ live --> catalog
232
+ catalog --> policy[agent allowlist → tool-loop]
232
233
  ```
233
234
 
234
- The manifest path is the **only** one that resolves `{{env.*}}` (at ingestion);
235
- every other write path requires a literal URL. Partial failure on the manifest
236
- path is isolated one malformed tool is reported in `errors[]` while the rest
237
- import. See [Tools](TOOLS.md#registering-a-tool).
235
+ Only manifest ingestion resolves `{{env.*}}` and `{{secret.*}}`; other data-tool
236
+ write paths require literal values. One malformed manifest tool is reported in
237
+ `errors[]` while valid entries import. MCP tools are never converted to stored
238
+ HTTP data tools. See [Tools](TOOLS.md#registering-a-tool).
238
239
 
239
240
  ## Durability: checkpoints and resume
240
241
 
@@ -316,7 +317,7 @@ validator as the after-task hook, so both roots enforce content safety identical
316
317
  | Recovery | `lib/insika/recovery.rb` |
317
318
  | Inbound queue (one turn at a time per session, and what happens to a message that arrives while one is running) | `lib/insika/session_actor.rb`, `lib/insika/queue_policy.rb`, `lib/insika/steer_injector.rb` |
318
319
  | Channels (a way in and out for people; the reply that travels after the turn ends) | `lib/insika/channel_registry.rb`, `lib/insika/channels/*`, `lib/insika/channel_delivery.rb`, `lib/insika/outbox_store.rb`, `lib/insika/inbound_log.rb` |
319
- | Tools (data/manifest/MCP) | `lib/insika/tool_definition.rb`, `tool_manifest.rb`, `mcp_tool_ingestor.rb` |
320
+ | Tools (data/manifest/MCP) | `lib/insika/tool_definition.rb`, `lib/insika/tool_manifest.rb`, `lib/insika/mcp_tool_registry.rb`, `lib/insika/tools/present.rb` |
320
321
  | Plugin loading (boot) | `lib/insika/plugin.rb`, `lib/insika/plugin/loader.rb` |
321
322
  | Refinement (traffic → report) | `lib/insika/refinement/*`, `lib/insika/refinement_store.rb` |
322
323
  | Post-turn learning (facts, skills, knowledge — extracted from finished conversations) | `lib/insika/distill.rb`, `lib/insika/harvest.rb`, `lib/insika/knowledge.rb`, `lib/insika/knowledge_store.rb`; the per-turn hook lives in `Executor#persist_turn`, next to `finalize_delegation` |
data/docs/ARTIFACTS.md CHANGED
@@ -67,6 +67,48 @@ No script, no external fetch, no forms. The model writes HTML with **inline
67
67
  SVG** for charts — that is a skill instruction (palette, tables, pure-SVG bars),
68
68
  not engine code. A "real" charting need is a plugin.
69
69
 
70
+ ## Reasoning effort on a report turn
71
+
72
+ A report turn is not one shape of work — it plans, then it mines, then it
73
+ writes. `thinking` is one value per agent for the whole turn, so an agent set to
74
+ `high` pays deliberation on every one of the 30–50 tool calls a real report
75
+ makes, and that is where the 300 s turn timeout gets spent.
76
+
77
+ Split the phases across agents instead, which the engine already supports today:
78
+
79
+ ```ruby
80
+ Insika.system do
81
+ # The miners: one narrow question each, no judgement to make.
82
+ agent("sales_miner") do
83
+ model "deepseek-v4-flash"
84
+ params thinking: "low"
85
+ tools %w[query_sales]
86
+ instructions "Answer ONE question about sales from the store data. Numbers, no prose."
87
+ end
88
+
89
+ # The orchestrator: it plans the report and writes it. This is the turn
90
+ # that deserves the deliberation.
91
+ agent("reporter") do
92
+ model "deepseek-v4-flash"
93
+ params thinking: "high"
94
+ tools %w[save_artifact]
95
+ subagents "sales_miner"
96
+ instructions "Plan the report, call spawn_subagents ONCE for every number you need, then write the page and save_artifact it."
97
+ end
98
+ end
99
+ ```
100
+
101
+ Two things make this work: a child inherits the *environment* (model, thinking)
102
+ only as a **default**, so its own `params thinking:` wins; and `spawn_subagents`
103
+ runs the children in parallel, so wall-clock is the slowest miner rather than
104
+ the sum. Each child also mines in its own isolated context, which is what keeps
105
+ the orchestrator's context from filling with raw rows.
106
+
107
+ Measure it before reaching for anything cleverer: the numbers that matter are
108
+ the turn's wall-clock, the timeout rate, and the judge score on the same report.
109
+ Per-phase effort *inside* a single turn is a real idea, but it is only worth
110
+ building once this recipe is shown not to be enough.
111
+
70
112
  ## Limits and retention
71
113
 
72
114
  - **Size cap** — `INSIKA_ARTIFACT_MAX_BYTES` (default 1 MB): an artifact is a
data/docs/CHANNELS.md CHANGED
@@ -149,16 +149,24 @@ Content-Type: application/json
149
149
  { "agent": "support", "session_id": "web:8f3c…", "message": "cadê meu pedido?" }
150
150
  ```
151
151
 
152
- The reply is SSE on that same connection — four frame types, and an unknown one is
152
+ The reply is SSE on that same connection — five frame types, and an unknown one is
153
153
  safe to ignore:
154
154
 
155
155
  ```
156
- event: delta data: {"delta":"Seu pedido "} the answer, token by token
156
+ event: delta data: {"delta":"Seu pedido chegou."} the published answer
157
157
  event: working data: {"name":"order_status"} a tool is running
158
+ event: ui data: {"component":"product_cards","title":"Chocolate","items":[{"id":"SKU-1","url":"https://shop.example/1","caption":"Dark chocolate"}]}
159
+ a presentation tool picked cards to show
158
160
  event: done data: {} the turn ended
159
161
  event: error data: {"message":"…"} it ended badly
160
162
  ```
161
163
 
164
+ The shipped widget renders `ui` as a plain list of caption + link under the
165
+ conversation (only an `http(s)` url becomes a link; any other scheme renders as
166
+ text); a host page that wants real cards restyles or replaces that — the
167
+ protocol is the frame. An empty selection sends `items: []`. Unlike `/v1/responses`,
168
+ the web frame omits `count` and `dropped`. See [presentation tools](TOOLS.md#presentation-tools-the-model-picks-ids-the-engine-shows-the-cards).
169
+
162
170
  **The engine issues the session id and the client never proposes one.** `POST
163
171
  /messages` with an id nobody minted is a `404`, never a new conversation: on an
164
172
  anonymous endpoint, create-on-write means anyone who guesses an id can read someone
@@ -315,6 +323,15 @@ stays internal unless the agent opts in. That contract is
315
323
  [the edge contract](ARCHITECTURE.md#what-crosses-the-edge), and it is why you can
316
324
  forward `content` straight to the customer.
317
325
 
326
+ The last balloon may also carry `attachments`: objects with `type`, `url`, `caption`
327
+ and an optional `id`,
328
+ the cards an evidence tool returned (see [Evidence](TOOLS.md#evidence-the-lean-envelope-and-grounding)).
329
+ When the turn called a **presentation tool**, the list is exactly the cards the model
330
+ selected, in call order, each also carrying the `component` and `title` of that call;
331
+ otherwise it is every card the turn's evidence tools returned. An empty presentation
332
+ selection suppresses that automatic fallback. Ignore the key if the consumer only
333
+ supports text.
334
+
318
335
  ### Delivery policy
319
336
 
320
337
  A relay declares **how the outbox flushes** — a property of the surface
data/docs/CONTEXT.md CHANGED
@@ -23,14 +23,16 @@ into a deterministic prompt:
23
23
  | Provider | Block | Priority | Layer | Notes |
24
24
  |----------|-------|:--------:|-------|-------|
25
25
  | **Identity** | system | **100 — pinned** | identity | The agent's prompt files (global system files first). Never cut. |
26
+ | **Fence notice** | system | **99 — pinned** | identity | Fixed instruction to treat contextual material as data; only with `fencing` on. |
26
27
  | **Skills** | `<available_skills>` | 80 | identity | Level-1 skill list, minus whatever is already eager — see [Skills](SKILLS.md). |
27
28
  | **Tool search** | `<available_tools>` | 70 | identity | Level-1 list of deferred tools — see [Tools](TOOLS.md). |
28
29
  | **Skill trigger** | `<active_skill>` | 85 | volatile | Level-2 bodies: the agent's `skills_eager` set, plus the ones whose `triggers:` match the message — see [Skills](SKILLS.md). |
29
30
  | **Knowledge** | `<knowledge>` | 77 | volatile | Level-1 top-K learned concepts for the turn's message (+ one-hop `[[links]]`), only if `knowledge.retrieve` is on. Cuttable — see [Knowledge](KNOWLEDGE.md). |
30
31
  | **Memory** | `<memory>` | 75 | volatile | Durable facts + recent notes, only if `memory` is on. Cuttable. |
31
- | **Briefing** | `<briefing>` | 65 | volatile | The session's working state (known fields, still-missing list, next step) — only if the pack declared `briefing_fields`. Cuttable. |
32
+ | **Briefing** | `<briefing>` | 65 | volatile | The session's working state — the *known* fields only. Only if the pack declared `briefing_fields`. Cuttable. |
32
33
  | **Session** | history | 60–79 | volatile | The running transcript; priority scales with recency. |
33
34
  | **Request** | `<request_context>` | 40 | volatile | Turn variables + tenant. Most cuttable; sits last. |
35
+ | **Briefing (tail)** | `<recitation>` | 95 | volatile | The still-missing list + next step, rendered **after the whole history** as a `user` message — the last thing the model reads before the current message. |
34
36
 
35
37
  The ordering is deliberate: the render order is **identity layer first, volatile
36
38
  layer after** — nothing volatile can sit above the cache boundary, whatever its
@@ -53,14 +55,37 @@ cacheable prefix byte-stable (see the prefix cache below).
53
55
  > returns empty turns, raise `context_budget` (e.g. to `60000`) before looking
54
56
  > anywhere else. See [Agents](AGENTS.md#default-limits).
55
57
 
56
- ### Compaction is not wired — except the mechanical dedupe
57
-
58
- There is a settings stub for LLM-summarization compaction (`enabled: false`,
59
- `keep_last`, a reserved utility-model slot), but **nothing consumes it today**
60
- — and the Studio no longer shows a form for it, so the setting cannot be
61
- switched on by accident. Size is managed purely by hard budget eviction.
62
- Do not rely on compaction to shrink a bloated agent: tune `context_budget` and
63
- keep the identity lean.
58
+ ### In-session compaction (opt-in, platform Settings)
59
+
60
+ When enabled (Studio Settings General, or the `compaction` settings hash),
61
+ the engine summarizes old turns *inside* the session instead of losing them to
62
+ eviction:
63
+
64
+ - **Trigger:** after a turn commits, if the session's *uncompacted* message
65
+ count exceeds `compact_after` (default 40), everything but the last
66
+ `keep_last` messages (default 20) is summarized by a cheap model
67
+ (`compaction.model`, falling back to the platform `utility_model`; neither
68
+ set = the feature is inert and `insika doctor` warns). Runs off the critical
69
+ path — the customer already has the answer.
70
+ - **Read path:** the Session provider replaces the compacted prefix with ONE
71
+ history fragment — a `user` message wrapped in `<conversation_summary>` tags,
72
+ priority 59 (one step below the oldest verbatim message), source
73
+ `compaction` (its own category in the context trace). The tail stays
74
+ verbatim and the boundary is **stable** between compactions, so the prompt
75
+ cache holds after it.
76
+ - **What survives:** the default prompt orders the summary to preserve customer
77
+ facts (sizes, CEP, order numbers), the assistant's commitments, the still-open
78
+ questions and the decisions already made; on re-compaction the previous
79
+ summary is folded in, so a fact from turn 3 survives every later batch.
80
+ A platform `compaction.prompt` replaces the default wholesale.
81
+ - **Scope:** store-sourced history only — a checkpoint resume replays its own
82
+ tape and an explicit `history` is the caller's contract; neither is
83
+ rewritten. Observability: the `:context_compacted` event, the
84
+ `insika.context.compacted` counter and `{upto, runs}` in the context trace.
85
+
86
+ Compaction does not replace the budget: eviction stays as the hard backstop
87
+ for a single oversized turn. Tune `context_budget` and keep the identity lean
88
+ regardless.
64
89
 
65
90
  One cheap half **is** wired, opt-in per agent: `tool_output_compression` (DSL
66
91
  `tool_output_compression`, or `"tool_output_compression": true` in the pack).
@@ -82,10 +107,10 @@ budget first.
82
107
  With `memory` enabled, an agent gains a built-in `remember` tool for durable
83
108
  facts, and those facts (plus recent notes) are injected back into the prompt on
84
109
  later turns — **including turns in a different session**. Memory is scoped per
85
- agent, per `(tenant, customer)` when the message carries a `customer`, and per
86
- session otherwise a session's own memory lives in a marked `memory:chat:<session id>`
87
- cell, never a bare one, so the Customers drill cannot read a conversation as a
88
- customer. This is distinct from *session history*, which is the transcript of one
110
+ `(tenant, customer)` when the message carries a `customer`. Without a customer,
111
+ it uses the tenant's cell; without either, it uses the session's marked
112
+ `memory:chat:<session id>` cell. A fresh session therefore does not isolate
113
+ memory when it still carries the same tenant. This is distinct from *session history*, which is the transcript of one
89
114
  conversation; memory is the small set of facts that should outlive any single
90
115
  conversation. Facts and notes are editable from the Studio agent page. See
91
116
  [`examples/memory/`](https://github.com/guizaols/insika/tree/main/examples/memory/) for a runnable cross-session example.
@@ -121,19 +146,37 @@ pack:
121
146
  briefing_fields "size", "budget", "delivery_day"
122
147
  ```
123
148
 
124
- With fields declared, the turn's `:system` context gains a `<briefing>` block
125
- (priority 65 — below identity/skill/memory so it never breaks the cacheable
149
+ With fields declared, the briefing renders in **two places**, and the split is
150
+ deliberate.
151
+
152
+ The durable half — what is already known — sits in the `:system` context
153
+ (priority 65: below identity/skill/memory so it never breaks the cacheable
126
154
  prefix, above the turn's own `<request_context>`):
127
155
 
128
156
  ```
129
157
  <briefing>
130
158
  known:
131
159
  size: M
160
+ </briefing>
161
+ ```
162
+
163
+ The half that is a *goal* — what is still missing and the agreed next step — is
164
+ **recited at the tail**, after the whole history, as the last thing the model
165
+ reads before the current user message:
166
+
167
+ ```
168
+ <recitation>
132
169
  still missing: budget, delivery_day
133
170
  next step: send the payment link tomorrow at 10
134
- </briefing>
171
+ </recitation>
135
172
  ```
136
173
 
174
+ Attention is strongest at the end of the context: a goal stated only at the top
175
+ is the first thing a 30-call turn forgets. So the recitation was **moved** there,
176
+ not copied — the head never repeats it, and the turn pays for it once. It rides
177
+ as a `user` message, like every other engine append inside a turn, so the system
178
+ identity prefix stays byte-stable.
179
+
137
180
  The `still missing` list is the point: the *model* sees which declared fields are
138
181
  still unanswered, so it stops re-asking for something already given. Stored keys
139
182
  that the pack no longer declares are never rendered. The Studio session screen
@@ -152,17 +195,21 @@ turns and resumes — a resumed conversation re-opens with the briefing intact.
152
195
 
153
196
  ## The provider prefix cache
154
197
 
155
- Two distinct caching mechanisms don't conflate them:
198
+ A stable prefix makes provider caching possible; eligibility, expiry and reported
199
+ savings still depend on the provider.
156
200
 
157
- - **Automatic server-side prefix cache.** Some providers prefix-cache a stable
158
- system prefix automatically, at no cost to configure. This works **only because**
159
- the engine renders the system in two layers (below) and the volatile half sits
160
- **under** the identity boundary, keeping the cacheable prefix byte-stable.
161
- Anything that injects volatile content high in the system block breaks the cache.
162
- - **Manual cache breakpoints (opt-in).** With `prompt_caching` on **and** a
163
- provider that supports explicit cache control, the builder sets one cache
164
- breakpoint at the end of the system block. Only enable this for a byte-stable
165
- system a volatile system turns every turn into a paid cache *write*.
201
+ - **Automatic prefix caching** needs no Insika flag. The identity-first render
202
+ order keeps changing memory and request data below the stable prefix.
203
+ - **Explicit cache breakpoints** are opt-in via `"prompt_caching": true` on
204
+ the agent profile and applied only when the resolved provider is `anthropic`. With both system layers present,
205
+ Insika sends two text blocks: identity with `cache_control`, then volatile text
206
+ without it. With no volatile text there is one block; with no identity there is
207
+ no breakpoint. Other providers receive a plain system string.
208
+
209
+ Custom context builders and hooks must keep `system_identity`, `system_volatile`
210
+ and their joined `system` consistent. Without a split, or after a hook replaces
211
+ only `system`, the chat builder falls back to treating the whole system as identity.
212
+ That fallback cannot protect a volatile suffix from cache invalidation.
166
213
 
167
214
  Cache accounting surfaces as `cached_tokens` (reads) and `cache_creation_tokens`
168
215
  (writes), visible in telemetry and the Studio tokens chip.
@@ -173,13 +220,14 @@ The system block is partitioned into two cache layers:
173
220
 
174
221
  - **Identity** — bytes that change only on deploy/config edit: the persona
175
222
  prompt (`Prompt`), the level-1 skill list (`Skill`) and the deferred-tool
176
- catalog (`ToolSearch`). This is the cacheable prefix.
223
+ catalog (`ToolSearch`), the fixed tool-discipline instructions and, when enabled,
224
+ the fencing notice. This is the cacheable prefix.
177
225
  - **Volatile** — bytes that may change per turn: memory, session history,
178
226
  triggered skill bodies, the `<request_context>`. Everything else.
179
227
 
180
228
  The layer is a **provider-class contract**, not profile data: `ContextProvider`
181
229
  declares `def layer = :volatile` (conservative — nothing gets pinned by
182
- accident) and the three identity builtins override to `:identity`. A pack does
230
+ accident) and the identity providers override to `:identity`. A pack does
183
231
  not set it — a pack reorganizes *which content goes into the Prompt provider vs
184
232
  the volatile providers*. The Builder stamps the layer on every fragment at
185
233
  production, and the render order is **identity first, volatile after** — a
@@ -194,13 +242,14 @@ output must be byte-stable across turns).
194
242
 
195
243
  ### The observable cache: fingerprints and the invalidation reason
196
244
 
197
- Each turn, the Executor hashes the rendered prefix into a PII-free fingerprint
198
- chain — one SHA-256 per system category in render order, one for the tool
199
- schemas, one cumulative `prefix` and compares it against the previous turn's
200
- entry. The **invalidation reason** is the first category whose bytes changed (or
201
- vanished); a turn whose prefix held reports nothing. History is deliberately
202
- excluded: a new user message is a divergence every turn, which would be noise,
203
- not a reason.
245
+ Each turn, the Executor hashes the rendered identity and tool schemas into
246
+ SHA-256 fingerprints and a cumulative `prefix`. The invalidation reason names
247
+ the first changed part of that stable prefix. Volatile system categories have
248
+ separate diagnostic digests: changes to memory, knowledge or request context do
249
+ not report an identity-prefix invalidation. History is excluded.
250
+
251
+ These fingerprints explain local prompt changes; they do not prove a provider
252
+ cache hit. Use the provider's token accounting for that.
204
253
 
205
254
  The Studio surfaces it in two places: the **session Context card** shows the
206
255
  turn's cache-hit percentage and the `broke: <category>` line (plus the
@@ -209,9 +258,8 @@ cache tab with the per-agent hit series over time. The per-agent series lives
209
258
  in its own capped store, because a session does not stamp its author — the
210
259
  per-session trace cannot answer "cache-hit over time for *this* agent".
211
260
 
212
- With the prefix stable by construction, the existing `prompt_caching` breakpoint
213
- sits on bytes that stay put the first (write) turn of a deployment pays the
214
- cache write once, every subsequent turn reads.
261
+ A stable identity can be reused while the provider cache remains eligible and valid.
262
+ A changed tool schema still invalidates the local prefix fingerprint.
215
263
 
216
264
  ## The volume
217
265
 
data/docs/DEPLOY.md CHANGED
@@ -25,7 +25,7 @@ is durable SQLite (WAL) at `INSIKA_DB`; mount a volume and point it inside.
25
25
  docker build -t insika .
26
26
  docker run -p 9292:9292 -v insika-data:/data \
27
27
  -e DEEPSEEK_API_KEY=sk-... \
28
- -e OPENCLAW_GATEWAY_TOKEN=change-me \
28
+ -e INSIKA_GATEWAY_TOKEN=change-me \
29
29
  insika
30
30
  curl localhost:9292/up # {"status":"ok"}
31
31
  ```
@@ -115,7 +115,7 @@ this section is the single source of truth for what changing it means.
115
115
  | `INSIKA_DRAIN_TIMEOUT` | `20` | seconds a stopping worker waits for in-flight turns before abandoning them to the next boot's recovery (process model, item 4). The entrypoint sizes Falcon's `--graceful-stop` from it; on Railway also set `RAILWAY_DEPLOYMENT_DRAINING_SECONDS` ≥ drain + 10 |
116
116
  | `INSIKA_TICK_INTERVAL` | `60` | seconds between tick passes — outbox drain + stale recovery sweep (process model, item 5). `0` disables |
117
117
  | `INSIKA_TICK_STALE_AFTER` | `900` | seconds a `:queued`/`:running` task must sit untouched before the tick sweeps it. Must exceed the largest `turn_timeout` of the deployment |
118
- | `OPENCLAW_GATEWAY_TOKEN` | falls back to `ADMIN_TOKEN` | Bearer for `/v1/responses` and `/v1/agents` (the API contract) |
118
+ | `INSIKA_GATEWAY_TOKEN` | falls back to `ADMIN_TOKEN` | Bearer for `/v1/responses` and `/v1/agents` (the API contract) |
119
119
  | `ADMIN_TOKEN` | `local-demo` | login token for `/studio` (**change in production**) |
120
120
  | `DEEPSEEK_API_KEY` | — | provider key. **Without it the engine still boots** (`/up` green), but turns fail until it is configured (env or Studio → LLM providers) — cloud resilience |
121
121
  | `DEEPSEEK_MODEL` | `deepseek-v4-flash` | model |
@@ -157,7 +157,7 @@ values (the API token falling back to `ADMIN_TOKEN` is a dev convenience only):
157
157
  - **`ADMIN_TOKEN`** — the `/studio` login (cookie auth). This is the **operator**
158
158
  surface (just you). Rotating it is **safe and independent**: change it, redeploy,
159
159
  log in with the new value. It does not affect any API consumer.
160
- - **`OPENCLAW_GATEWAY_TOKEN`** — the Bearer for `/v1/responses` and `/v1/agents`.
160
+ - **`INSIKA_GATEWAY_TOKEN`** — the Bearer for `/v1/responses` and `/v1/agents`.
161
161
  This is the **contract with your API consumers**. Rotating it means **changing
162
162
  both sides together** (or the integration breaks): update the runtime var **and**
163
163
  each consumer's token in the same step.
@@ -187,15 +187,35 @@ insika doctor # colored report; exits != 0 on any error
187
187
  insika doctor --json # machine-readable (CI / monitoring)
188
188
  insika doctor --fix # applies the safe autofixes and re-diagnoses
189
189
  insika env # lists known keys + current values (secrets masked)
190
+ insika tools:report # tool audit over the stored traces: never-called
191
+ # allowlisted tools, error rate > 30%, stale tools, gate refusals —
192
+ # read-only, the operator removes ([--agent ID] [--days N] [--json])
190
193
  ```
191
194
 
192
195
  Checks: env (the schema above), settings schema version (a pending migration →
193
196
  `--fix` applies it), a missing platform `default_model` (`--fix` seeds it from
194
197
  `DEEPSEEK_MODEL`), durable vs ephemeral backend, LLM provider configured,
195
- `ADMIN_TOKEN` set, data-tool definitions still valid, **prompt files that hold
198
+ `ADMIN_TOKEN` set, data-tool definitions still valid, **a stored agent whose
199
+ declared tool allow/deny list does not name the `tool_allowlist` policy** (the
200
+ engine repairs it on read, but as stored the list is inert — see
201
+ [Agents](AGENTS.md#the-allowlist-convention)), **prompt files that hold
196
202
  text rather than a serialized object** (a file whose content is a stringified Hash
197
203
  serves a mangled prompt on every turn while looking perfectly healthy — present,
198
- non-empty, and the agent still answers), and **skill drift** a shared skill whose
204
+ non-empty, and the agent still answers), **a prompt file that outgrew a prompt**
205
+ (WARN past ~6 000 estimated tokens or 600 lines — the LLM-generated pack shape
206
+ that costs 20%+ extra tokens per turn for no better instruction-following),
207
+ **in-session compaction enabled with no model slot** (WARN — neither
208
+ `compaction.model` nor the platform `utility_model` is set, so it can never run;
209
+ see [Context](CONTEXT.md)), **eval seeding left on** (WARN — `evals.seeding`
210
+ opens `POST /v1/conversations/:id/seed`, which writes a fabricated conversation
211
+ state under the tenant token; fine while running snapshot evals, off in
212
+ production — see [Evals](EVALS.md#state--a-case-starts-from-a-snapshot)),
213
+ **provenance and presentation tools without an allowed evidence data tool**
214
+ (WARN — the check only sees stored data declarations; verify any code-tool source),
215
+ **fencing off for agents exposed by relay/widget environment configuration**,
216
+ **cache-layer declarations** (known volatile providers marked identity are errors;
217
+ custom identity providers need a byte-stability review), and
218
+ **skill drift** — a shared skill whose
199
219
  body names one store, a prompt file routing to a skill the agent cannot load, a broken
200
220
  companion pair, a stale `eager:` key (see
201
221
  [Skills](SKILLS.md#drift-guards)). Settings-schema migrations are **explicit**
@@ -227,7 +247,7 @@ healthcheck, and a restart policy.
227
247
  2. **Volume**: mount it at `/data` (the default `INSIKA_DB` points there) —
228
248
  without a volume, SQLite is ephemeral and recovery resumes nothing after a
229
249
  redeploy.
230
- 3. **Vars**: `DEEPSEEK_API_KEY`, `OPENCLAW_GATEWAY_TOKEN`, `CONSUMER_INTERNAL_URL`,
250
+ 3. **Vars**: `DEEPSEEK_API_KEY`, `INSIKA_GATEWAY_TOKEN`, `CONSUMER_INTERNAL_URL`,
231
251
  `INSIKA_EGRESS_HOSTS`. **Leave `WEB_CONCURRENCY` at its default of 1** unless
232
252
  you run [`insika-router`](ROUTER.md) in front. Railway's own docs say it
233
253
  "does not support sticky sessions" and randomly distributes traffic across
@@ -402,7 +422,7 @@ cache hits, P50/P95, error rate. Runs against local or a remote deployment. See
402
422
  [LOADTEST.md](LOADTEST.md).
403
423
 
404
424
  ```bash
405
- INSIKA_URL=http://localhost:9292 OPENCLAW_GATEWAY_TOKEN=xxx \
425
+ INSIKA_URL=http://localhost:9292 INSIKA_GATEWAY_TOKEN=xxx \
406
426
  bundle exec ruby scripts/loadtest.rb --agents assistant --concurrency 16 --iterations 3
407
427
  ```
408
428
 
data/docs/EVALS.md CHANGED
@@ -42,6 +42,99 @@ expect:
42
42
  Turns replay **in order** under one conversation, so a case can build context ("what
43
43
  about the shipping?" after "I want the 70% bar"); the assertions run on the last turn.
44
44
 
45
+ ### State — a case starts from a snapshot
46
+
47
+ Every case above starts from an **empty** conversation. To test "the customer already
48
+ saw three products and says *add the second one*", such a case would first have to
49
+ replay the search turn — which makes it depend on the model's first answer, costs a
50
+ turn, and cannot reproduce a messy state (a contradiction from six turns ago, a
51
+ stored preference). `state:` is the precondition, loaded into the conversation
52
+ **before turn 1**:
53
+
54
+ ```yaml
55
+ id: loja-chocolates-add-seen
56
+ agent: loja-chocolates
57
+ requires:
58
+ tools: [add_to_cart]
59
+ state:
60
+ evidence:
61
+ ids: ["SKU-70-DARK"]
62
+ cards:
63
+ - { type: card, url: "https://shop.example/70", id: "SKU-70-DARK", caption: "Dark chocolate" }
64
+ memory: { facts: { preference: "dark chocolate" }, notes: [] }
65
+ history:
66
+ - { role: user, content: "quero chocolate amargo" }
67
+ - { role: assistant, content: "O chocolate 70% é SKU-70-DARK." }
68
+ briefing: { fields: { cep: "01311-000" } }
69
+ turns:
70
+ - user: "adiciona uma unidade desse chocolate 70% no carrinho"
71
+ expect:
72
+ tools_called: [add_to_cart]
73
+ never_calls: [search_products]
74
+ reply_omits: ["SKU-70-DARK"]
75
+ ```
76
+
77
+ Only those four `state` keys are accepted by the case loader. A persona case may
78
+ carry `state` too. Adapt the tool names, arguments and briefing fields to the agent.
79
+
80
+ The evidence ledger is a runtime precondition, not a list shown to the model:
81
+ include the relevant product and ID in `history` or the user's request. `cards`
82
+ seeds the cards a search would have returned (each needs a `url` and an `id`; the
83
+ id counts as seen), so a presentation case needs no lookup in the turn.
84
+
85
+ With the HTTP transport, the eval stays a **client**: the runner never writes a
86
+ store. Seeding goes through
87
+ `POST /v1/conversations/:id/seed` (same Bearer as the turn, same id namespacing for a
88
+ tenant), and the deployment accepts it **only while the platform setting
89
+ `evals.seeding` is on** — off by default, because a seeded conversation is a
90
+ fabricated precondition, and the doctor warns while it is on. With the setting off
91
+ the replay Runner **skips** seeded cases with a reason; it also skips when its
92
+ transport has no seed support. Other seed errors fail the replay. In-process
93
+ `GraphTransport` dispatches `seed_session` directly and bypasses `evals.seeding`;
94
+ it needs a runtime exposing the graph. The persona
95
+ Simulator surfaces seed refusal as an error instead of the Runner's skip result.
96
+
97
+ A session with messages returns `409`. Replays default to `eval-<case-id>`; pass
98
+ `--conv-map FILE` with a JSON mapping such as
99
+ `{"loja-chocolates-add-seen":"eval-add-seen-run-2"}` to use a fresh ID on reruns.
100
+ Seeded memory may share a tenant cell even with fresh IDs; see
101
+ [the seed API](API.md#seeding-an-eval-conversation) for scope and payload details.
102
+
103
+ #### Graders — what a turn's calls and reply are checked against
104
+
105
+ The turn's SSE stream reports each tool call with its arguments and how it ended
106
+ (`ok`, `error`, or `blocked` plus the gate that held it), so the deterministic
107
+ layer can check more than "was the tool called". All optional; each is its own
108
+ check, and the report names the one that failed.
109
+
110
+ | Key | Checks |
111
+ |-----|--------|
112
+ | `tools_called: [name, name?]` | each required tool was called (`?` = optional, never fails) |
113
+ | `never_calls: [names]` | none of these was called — the negative every `tools_called` needs |
114
+ | `calls_one_of: [names]` | at least one of these was called |
115
+ | `first_tool: name` | the first call's name |
116
+ | `max_tool_calls: N` | a ceiling on the turn's calls |
117
+ | `reply_includes: [substrings]` | each appears in the published answer (case-insensitive) |
118
+ | `reply_omits: [substrings]` | none appears — where an internal id, a CPF or a raw tag leaking into the customer's text is pinned |
119
+ | `blocked_gates: ["tool:gate"]` | each pair appears among the turn's blocked calls |
120
+ | `ui_components: [names]` | a presentation tool showed at least one card of each component (the `insika.ui` frames) |
121
+ | `no_ui: true` | the turn showed nothing — the negative of `ui_components` |
122
+ | `must_not: [detectors]` | the negative detectors (`pii_leak`, `tool_error`, …); a blocked call is not a tool error |
123
+
124
+ **Every positive has a negative.** A case that only says `tools_called: [add_to_cart]`
125
+ passes an agent that also re-searched, or that echoed the SKU to the customer. Pin
126
+ what a correct turn does *not* do — `never_calls`, `reply_omits`, `max_tool_calls` —
127
+ in the same case. Snapshots avoid the model calls needed to reconstruct the setup;
128
+ use the simulator when later customer messages need to branch on the replies.
129
+
130
+ `tools_called` confirms an attempted call, not a successful backend mutation.
131
+ Arguments are collected for inspection; there is no generic argument or cart-state
132
+ grader. `blocked_gates` needs completion statuses from the HTTP transport. The
133
+ in-process `GraphTransport` records tool names and UI events, but not completion
134
+ statuses or arguments. Neither transport counts `load_skill` / `load_knowledge` as
135
+ tool calls. `ui_components` requires a nonempty selection; `no_ui`
136
+ accepts absent UI events and events with zero items.
137
+
45
138
  ### `requires` — a case that cannot run here is skipped, not failed
46
139
 
47
140
  Deployments differ. Some stores have order tracking wired, some do not; some run
@@ -269,14 +362,11 @@ The model only sees `case_id`, enumerated with the ids the tool can actually run
269
362
  (never a free string it could invent) — every **simulated** case in the store, the same
270
363
  `persona:` shape as above.
271
364
 
272
- **Safety is derived here too, but there is no swap yet.** The tool computes the
273
- target's reachable side-effect tools (`Evals::EvalProfile`, the same derivation the CLI
274
- uses) and **refuses outright** naming the tools if that list is non-empty. Unlike
275
- the CLI, nothing here actually swaps a side-effect tool for a dry-run: `Evals::
276
- EvalProfile.registry` (the overlay) exists for exactly that, but nothing calls it yet.
277
- So `run_persona_eval` only runs against **read-only** target agents today; wiring the
278
- overlay into an in-process turn (so a target WITH a write tool can be tested safely) is
279
- follow-up work, not something this tool claims to do.
365
+ **Side effects are replaced for local graph runs.** The tool derives the target's
366
+ reachable side-effect tools and builds a throwaway executor with recorder tools
367
+ under those names. Read-only tools and the other graph collaborators are shared.
368
+ If side effects are reachable but no graph was supplied, it refuses the run.
369
+ This substitution prevents real writes; it does not prove a backend mutation.
280
370
 
281
371
  **Budget**: the persona model + judge model calls are the cost of running the eval,
282
372
  charged to the **calling** agent's own turn — never the target's (the target's own