insika 0.8.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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +75 -0
  3. data/README.md +5 -3
  4. data/bin/insika +1 -1
  5. data/docs/AGENTS.md +52 -11
  6. data/docs/API.md +73 -0
  7. data/docs/ARCHITECTURE.md +45 -44
  8. data/docs/CHANNELS.md +19 -2
  9. data/docs/CONTEXT.md +33 -27
  10. data/docs/DEPLOY.md +13 -2
  11. data/docs/EVALS.md +98 -8
  12. data/docs/FACTS.md +4 -0
  13. data/docs/KNOWLEDGE.md +7 -0
  14. data/docs/OBSERVABILITY.md +21 -6
  15. data/docs/POLICY.md +4 -1
  16. data/docs/RELEASING.md +4 -0
  17. data/docs/SECURITY.md +27 -1
  18. data/docs/TOOLS.md +127 -33
  19. data/docs/prompts/ADD-TOOL.md +12 -2
  20. data/docs/prompts/DIAGNOSE-TURN.md +3 -0
  21. data/docs/prompts/GO-LIVE.md +3 -1
  22. data/lib/insika/agent_profile.rb +21 -9
  23. data/lib/insika/channels/web/widget.js +33 -0
  24. data/lib/insika/channels/web.rb +5 -2
  25. data/lib/insika/chat_builder.rb +62 -20
  26. data/lib/insika/commands/agent_payload.rb +1 -1
  27. data/lib/insika/commands/run_distillation.rb +5 -8
  28. data/lib/insika/commands/seed_session.rb +118 -0
  29. data/lib/insika/context/builder.rb +29 -9
  30. data/lib/insika/context/priority.rb +2 -0
  31. data/lib/insika/context/provider.rb +5 -0
  32. data/lib/insika/context/providers/briefing.rb +11 -8
  33. data/lib/insika/context/providers/fence_notice.rb +27 -0
  34. data/lib/insika/context/providers/knowledge.rb +7 -4
  35. data/lib/insika/context/providers/memory.rb +8 -4
  36. data/lib/insika/context/providers/session.rb +7 -3
  37. data/lib/insika/doctor.rb +109 -1
  38. data/lib/insika/dsl/runtime.rb +1 -0
  39. data/lib/insika/dsl.rb +6 -0
  40. data/lib/insika/edge_limiter.rb +4 -1
  41. data/lib/insika/errors.rb +1 -0
  42. data/lib/insika/evals/assertions.rb +92 -6
  43. data/lib/insika/evals/golden.rb +91 -2
  44. data/lib/insika/evals/runner.rb +20 -0
  45. data/lib/insika/evals/simulator.rb +11 -2
  46. data/lib/insika/evals/transport.rb +117 -15
  47. data/lib/insika/evidence.rb +79 -12
  48. data/lib/insika/executor.rb +30 -23
  49. data/lib/insika/fence.rb +96 -0
  50. data/lib/insika/golden_store.rb +3 -0
  51. data/lib/insika/mcp_store.rb +5 -2
  52. data/lib/insika/mcp_tool_registry.rb +8 -1
  53. data/lib/insika/memory_store.rb +12 -0
  54. data/lib/insika/overlay_tool_registry.rb +5 -0
  55. data/lib/insika/prefix_fingerprint.rb +32 -27
  56. data/lib/insika/profile_source.rb +1 -0
  57. data/lib/insika/server/app.rb +43 -1
  58. data/lib/insika/server/rack_app.rb +2 -0
  59. data/lib/insika/server/responses.rb +31 -4
  60. data/lib/insika/session_store.rb +4 -1
  61. data/lib/insika/settings_store.rb +10 -1
  62. data/lib/insika/spoken_transcript.rb +31 -0
  63. data/lib/insika/studio/app.rb +6 -2
  64. data/lib/insika/studio/forms.rb +18 -3
  65. data/lib/insika/studio/views/_agent_tab_config.erb +5 -1
  66. data/lib/insika/studio/views/session.erb +1 -1
  67. data/lib/insika/studio/views/tool_edit.erb +6 -2
  68. data/lib/insika/telemetry/recorder.rb +13 -1
  69. data/lib/insika/tool_assembly.rb +21 -13
  70. data/lib/insika/tool_definition.rb +73 -10
  71. data/lib/insika/tool_envelope.rb +102 -2
  72. data/lib/insika/tool_store.rb +9 -4
  73. data/lib/insika/tool_trace_store.rb +1 -1
  74. data/lib/insika/tool_usage_report.rb +12 -2
  75. data/lib/insika/tools/data_defined_tool.rb +1 -0
  76. data/lib/insika/tools/present.rb +122 -0
  77. data/lib/insika/tools/run_persona_eval.rb +6 -1
  78. data/lib/insika/tools/tool_search.rb +4 -2
  79. data/lib/insika/turn_state.rb +13 -1
  80. data/lib/insika/version.rb +1 -1
  81. data/lib/insika/wiring/graph.rb +7 -0
  82. data/lib/insika/wiring/graph_chat.rb +4 -0
  83. data/lib/insika.rb +4 -0
  84. metadata +6 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 345e57c18a277e8ecd49b2481300c1474c60b2e0c3c384a086740e0de5ec3a2e
4
- data.tar.gz: 800794431e279fec34f135e4e203a0488b04066b667bc691806ea5f575d81530
3
+ metadata.gz: ec89eb1dae94e5a57733d0dd88a19d92ff98eb7e2ae0b907792e2a4ce8d508aa
4
+ data.tar.gz: cb882c8015e00c0c905e5945a05a5a465c938dedda762914c3737a64a9f0d4fb
5
5
  SHA512:
6
- metadata.gz: 199cc16b9ea651ec2da1d2e6f7ddea5020194882da00cf96981e8d5d44086233862873d14640d11cd1d445f9d202992ed46691210f9fb1699b1a3d56a99b3145
7
- data.tar.gz: 44d24bc53036da3803ee6eba6203d1addc8ccf0bd27c1aa15d1e9acfe763bca807ede55c2b7136ee2c245e5cd725541c719baa018c99e09638e964803bf688a9
6
+ metadata.gz: e2acb07f2ae896ee762cd3f1113dca3d462f80ea0bb32caea69f5db3fd6fbd31821cf9a5140b32c64c26b702b29b78aaf9fec0ca8f59653178dcb37afe9eca38
7
+ data.tar.gz: f8c98a2b76c802a75e18a77e0762ef42c650aa956afb3f131e1e9ccacfb2a56de87540b7389c740021008b45a174fa371d7a6dbb0a5a7cf83f248ce41b696770
data/CHANGELOG.md CHANGED
@@ -8,6 +8,81 @@ it is released. Entries land with the pull request that makes the change.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.9.0] - 2026-09-07
12
+
13
+ The commerce-agent wave: the engine now enforces what the prompt used to ask for —
14
+ a write names only IDs a lookup returned, the model picks which cards to show and
15
+ the engine shows them, third-party text can be fenced before the model reads it,
16
+ and an eval case can start from a seeded snapshot. Additive on the wire: `/v1`
17
+ gains frames and keys, none change meaning.
18
+
19
+ ### Added
20
+
21
+ - **Write provenance** — `requires_evidence` on HTTP data tools checks declared
22
+ ID parameters against the session evidence ledger before approval or execution.
23
+ Unknown IDs and an unavailable ledger return `blocked` with gate `provenance`.
24
+ Studio editing, Doctor diagnostics, session traces, `tool_blocked`, the
25
+ `insika.tool.blocked` counter and `insika tools:report` expose the gate.
26
+ See [Tools](docs/TOOLS.md#provenance-checking-ids-before-a-write).
27
+ - **Snapshot evals** — cases can seed evidence IDs, memory, user/assistant history
28
+ and briefing before the first turn. The authenticated seed endpoint is disabled
29
+ by default (`evals.seeding`); Doctor warns when enabled. New assertions cover
30
+ forbidden/alternative/first calls, call limits, reply text and blocked gates.
31
+ Responses SSE now carries tool arguments, completion status and the provider's
32
+ `call_id` on both item frames, so a consumer pairs `added` with `done`.
33
+ See [Evals](docs/EVALS.md#state--a-case-starts-from-a-snapshot).
34
+ - **Presentation tools** — data tools with `presentation` instead of `request`
35
+ select evidence cards by known ID — this turn's, or the last 64 the session
36
+ ledger kept (snapshots seed them as `evidence.cards`). They report shown/dropped IDs,
37
+ emit `insika.ui` (web channel: `ui`) and select the outbox attachments. Evidence
38
+ cards carry IDs; empty selections suppress automatic card delivery. Includes
39
+ `ui_components` / `no_ui` eval assertions and Doctor diagnostics.
40
+ See [Tools](docs/TOOLS.md#presentation-tools-the-model-picks-ids-the-engine-shows-the-cards).
41
+ - **Fencing** — opt-in per agent through the DSL, payload or Studio. Sanitizes
42
+ ordinary tool result strings, memory text and injected knowledge summaries, and adds a fixed
43
+ identity-layer notice. The tool-string cap `fencing.max_chars` defaults to 12,000; the agent flag
44
+ defaults to off — and stays off in this release: flipping the default waits for the
45
+ golden-case comparison with fencing on ([Releasing](docs/RELEASING.md), gate 4).
46
+ Doctor warns for unfenced agents exposed by relay/widget env configuration.
47
+ See [Agents](docs/AGENTS.md#fencing--third-party-text-is-data-never-instructions).
48
+
49
+ ### Changed
50
+
51
+ - **Cache boundary** — explicit Anthropic cache control ends at the identity layer;
52
+ volatile system text follows in a separate block. Prefix fingerprints cover
53
+ identity and tool schemas; memory/knowledge/request changes retain separate
54
+ diagnostic digests without reporting identity-prefix invalidation.
55
+ See [Context](docs/CONTEXT.md#the-provider-prefix-cache).
56
+ - **Serial writes in parallel batches** — tools marked `side_effect` acquire a
57
+ per-session serial gate before the shared concurrency slot. Independent reads
58
+ can overlap writes; queued writes do not consume that shared slot. MCP tools
59
+ annotated `readOnlyHint` are reads.
60
+ - Memory and knowledge extraction read only nonblank user/assistant text, preserving
61
+ original transcript indexes. Direct tool messages are excluded; assistant
62
+ paraphrases can still reach extraction. This applies regardless of `fencing`.
63
+ - A tool's own `{"status":"blocked"}` answer is a call that ran: only the engine's
64
+ gate refusal reports `blocked` on `:tool_result`, the trace and `tools:report`.
65
+ - A Studio save that changes a tool's HTTP method re-derives `side_effect`.
66
+
67
+ ### Fixed
68
+
69
+ - Data tools declaring `evidence` no longer fail when the envelope parses an
70
+ already-parsed `Evidence::Spec`.
71
+ - Studio tool saves preserve `evidence` and `presentation` declarations.
72
+ - `scripts/serve_real.rb` passes channel and settings stores to the edge, enabling
73
+ configured relay/widget routes and the eval seeding setting in the demo server.
74
+ - Fencing: the notice ships with the flag regardless of the `context_providers`
75
+ allowlist; briefing values and the compaction summary are sanitized too; evidence
76
+ item ids stay byte-exact for the model (only the line is sanitized).
77
+ - Provenance: a schema-optional parameter the model omits is not checked; a blocked
78
+ call on a one-shot turn no longer fails emitting `tool_blocked`.
79
+ - `POST /v1/conversations/:id/seed` decodes a percent-encoded conversation id.
80
+ - Widget `ui` cards link only `http(s)` urls.
81
+ - The evidence ledger reads the session row once per turn, not once per gated call.
82
+ - Studio reads its templates as UTF-8 regardless of the process locale: a host with
83
+ no `LANG` answered 500 on the login page (found by the release install proof).
84
+
85
+
11
86
  ## [0.8.0] - 2026-08-31
12
87
 
13
88
  The post-consolidation hardening wave: the last two OpenClaw-era names are gone
data/README.md CHANGED
@@ -18,7 +18,9 @@ deployment.
18
18
  - **Drop-in `/v1/responses`** — SSE streaming and usage, the shape existing clients already speak.
19
19
  - **Durable & resumable** — every turn checkpoints; a crash resumes without repeating side-effects.
20
20
  - **Agents as data** — agents, prompts, skills and tools are editable at runtime (UI or API), no redeploy.
21
- - **Tools & skills** — code tools, data-defined tools, MCP import; skills load on demand.
21
+ - **Tools & skills** — code, HTTP data and live MCP tools; skills load on demand.
22
+ - **Evidence-backed actions** — ID checks before writes, serial side effects per session, and selected evidence cards ([Tools](docs/TOOLS.md)).
23
+ - **Snapshot evals** — seed a case's starting state and check calls, refusals, replies and cards ([Evals](docs/EVALS.md)).
22
24
  - **Safe by default** — content-safety guardrails, an egress guard, confined execution, approvals, edge limits.
23
25
  - **Observable** — an event stream, per-session tool-call traces, optional OpenTelemetry.
24
26
  - **~0.4 ms of engine per turn** — p50 overhead on a neutral, key-free benchmark you can rerun yourself ([methodology](docs/BENCHMARK.md)).
@@ -106,7 +108,7 @@ navigates by.
106
108
 
107
109
  - [Agents](docs/AGENTS.md) — the AgentProfile and every key on it; create and edit at runtime.
108
110
  - [Limits and policy](docs/POLICY.md) — the five layers: allowed tools, approvals, guardrails, edge limits, reasoning.
109
- - [Tools](docs/TOOLS.md) — code vs data vs MCP tools, manifests, egress troubleshooting.
111
+ - [Tools](docs/TOOLS.md) — code, HTTP, MCP and presentation tools; evidence gates and serial writes.
110
112
  - [Skills](docs/SKILLS.md) — the SKILL.md format and progressive loading.
111
113
  - [Context](docs/CONTEXT.md) — what fills a turn's prompt; budget, eviction, memory.
112
114
  - [Workflows](docs/WORKFLOWS.md) — deterministic orchestration of several agents: the five patterns, and when to let the model choose instead.
@@ -140,7 +142,7 @@ navigates by.
140
142
 
141
143
  **Improve**
142
144
 
143
- - [Evals](docs/EVALS.md) — the cases that grade an agent: rubrics, the judge panel, and the pre-merge gate.
145
+ - [Evals](docs/EVALS.md) — snapshot cases, tool/UI assertions, rubrics, the judge panel and the pre-merge gate.
144
146
  - [Refinement](docs/REFINEMENT.md) — read an agent's own traffic back as a ranked report of what broke.
145
147
  - [Outcomes and follow-ups](docs/OUTCOMES.md) — what the traffic was worth in business terms, and the tool that comes back on a promise.
146
148
  - [Knowledge](docs/KNOWLEDGE.md) — extract durable concepts from finished conversations into a per-agent knowledge base; provenance-stamped, PII-redacted, best-effort with a re-scan recovery path.
data/bin/insika CHANGED
@@ -390,7 +390,7 @@ def evals_simulate(argv)
390
390
  # paired comparison, which it silently invalidates.
391
391
  require "securerandom"
392
392
  conv = opts[:conv] || "sim-#{golden.id}-#{SecureRandom.hex(4)}"
393
- run = simulator.run(persona: golden.persona, agent: target_agent, conv: conv)
393
+ run = simulator.run(persona: golden.persona, agent: target_agent, conv: conv, state: golden.state)
394
394
 
395
395
  verdict = judge && judge.score_conversation(
396
396
  rubric: golden.rubric, transcript: run.transcript, policy: golden.policy,
data/docs/AGENTS.md CHANGED
@@ -144,9 +144,44 @@ the thing worth declaring:
144
144
  tool_persistence false # remove the block for this agent
145
145
  ```
146
146
 
147
- The block is a byte-stable constant, so `prompt_caching` stays effective: the
148
- deploy that introduces it costs one cache write per agent, and every turn after
149
- that hits as before.
147
+ The block is constant, so it does not invalidate the identity prefix between turns.
148
+
149
+ ### `prompt_caching` explicit cache control
150
+
151
+ Set `"prompt_caching": true` in the agent payload, or enable it in the Studio
152
+ agent configuration. There is no dedicated DSL method for this flag.
153
+
154
+ For Anthropic, this puts the cache breakpoint after the identity layer and leaves
155
+ volatile system text below it. Other providers receive a plain system string and
156
+ may cache automatically. See [Context](CONTEXT.md#the-provider-prefix-cache) for
157
+ layering, fallback behavior and token accounting.
158
+
159
+ ### `fencing` — third-party text is data, never instructions
160
+
161
+ ```ruby
162
+ fencing true
163
+ ```
164
+
165
+ Off by default. When enabled, the engine sanitizes ordinary tool result string
166
+ leaves after evidence processing, memory facts/notes, briefing values and the next
167
+ step, the compaction summary, and the knowledge names and descriptions injected in
168
+ the context block. It normalizes Unicode (NFKC), removes invisible format characters,
169
+ replaces control characters except tabs/newlines/carriage returns, strips known
170
+ engine/transcript tag shapes and defuses forged role markers. Tool result strings
171
+ are also capped at `fencing.max_chars` (platform default 12,000); memory and knowledge
172
+ context sanitization does not apply that cap. Tool error and halt results bypass
173
+ this sanitizer; tool Hash keys, evidence item `id`s and attachment captions are
174
+ unchanged (an id is a key the ledger holds byte-exact).
175
+ `load_knowledge` returns the full body without fencing.
176
+
177
+ A fixed notice beneath the identity tells the model to treat contextual material
178
+ as data. It ships with the flag — the `context_providers` allowlist does not govern
179
+ it. It is stable across turns; it does not guarantee prompt-injection resistance.
180
+
181
+ Memory/knowledge extraction reads only nonblank user/assistant text regardless
182
+ of the flag. Direct tool messages are excluded, but text repeated by the
183
+ assistant can still reach extraction. `insika doctor` warns for unfenced agents
184
+ exposed by the configured relay/widget environment settings.
150
185
 
151
186
  ### Why some limits are missing from that list
152
187
 
@@ -173,17 +208,16 @@ So the two groups read differently on purpose:
173
208
  ### `tool_concurrency` — parallel tool calls
174
209
 
175
210
  When the model asks for several tools in one step, they run **one at a time by
176
- default**. Raise `tool_concurrency` and they run together, capped at that number
177
- in flight:
211
+ default**. Raise `tool_concurrency` to overlap independent reads, capped at that
212
+ number in flight. Tools marked `side_effect` still execute one at a time per session:
178
213
 
179
214
  ```ruby
180
215
  limit :tool_concurrency, 4 # nil / 0 / 1 = serial (the default); N = at most N at once
181
216
  ```
182
217
 
183
218
  One number is both the switch and the cap. It pays off only when a turn issues
184
- several **slow, independent** calls (data tools waiting on HTTP) the wall-clock
185
- becomes the slowest call instead of the sum. It buys nothing for fast in-process
186
- tools, and it is the *model* that decides the fan-out, which is why the cap is not
219
+ several **slow, independent reads** (data tools waiting on HTTP). It buys nothing
220
+ for fast in-process tools, and it is the *model* that decides the fan-out, which is why the cap is not
187
221
  optional: an uncapped batch of 15 data tools is 15 simultaneous requests to the
188
222
  same backend.
189
223
 
@@ -405,9 +439,9 @@ Three capabilities invert the default — `nil`/absent means **OFF**, not "all":
405
439
  `subagents`, `memory`, and `guardrails` (each defaults to off or a conservative
406
440
  setting, never "everything on"). `tool_output_compression` is a fourth: opt-in
407
441
  mechanical dedupe of repeated tool results in the history (see
408
- [Context](CONTEXT.md#compaction-is-not-wired--except-the-mechanical-dedupe)),
409
- off by default because it changes what the model sees. And one flag inverts the
410
- other way: `tool_persistence` is **ON unless you set it to `false`** (see
442
+ [Context](CONTEXT.md#budget-and-eviction--the-actual-compaction)),
443
+ off by default because it changes what the model sees; `fencing` is a fifth (see
444
+ [`fencing`](#fencing--third-party-text-is-data-never-instructions)), off by default. One flag inverts the other way: `tool_persistence` is **ON unless you set it to `false`** (see
411
445
  [`tool_persistence`](#tool_persistence--dont-give-up-on-the-first-empty-result)).
412
446
 
413
447
  ### Declaring what this deployment has
@@ -692,6 +726,13 @@ optionally shared outside the Studio via an expiring signed link
692
726
  saving agent — never a parameter the model types. Artifact content is LLM
693
727
  output and is served as untrusted. See [Artifacts](ARTIFACTS.md).
694
728
 
729
+ ## Review before enabling writes
730
+
731
+ Does each ID come from the server? Declare `evidence` on the lookup tool and
732
+ `requires_evidence` on the write tool so the engine enforces session provenance.
733
+ Mark writes as `side_effect` so parallel batches serialize them and recovery skips
734
+ completed calls. See [Tools](TOOLS.md#provenance-checking-ids-before-a-write).
735
+
695
736
  ## See also
696
737
 
697
738
  - [Tools](TOOLS.md) — define, register, and troubleshoot tools.
data/docs/API.md CHANGED
@@ -21,6 +21,7 @@ two cannot drift (a spec pins the gate's version to the date below).
21
21
  | `POST /v1/agents` | provisioning by definition/pack (idempotent, authoritative) |
22
22
  | `GET/DELETE /v1/agents` | list / remove agents |
23
23
  | `POST /v1/sessions` | create a session explicitly |
24
+ | `POST /v1/conversations/:id/seed` | load an eval snapshot before the first turn; see [Seeding](#seeding-an-eval-conversation) |
24
25
  | `POST /v1/outcomes` | record a business outcome (`conversion`, `escalation`, any label) |
25
26
  | `GET /v1/outcomes` | last outcome per agent + the per-day series |
26
27
  | `POST /v1/commands/...` | operator commands (`forget_customer`, `delete_tenant_data`, the follow-up mutations) |
@@ -30,6 +31,78 @@ two cannot drift (a spec pins the gate's version to the date below).
30
31
  The turn endpoints speak the OpenAI `response.completed` wire format; the
31
32
  `Insika-Version` header declares the compatibility vintage of a request.
32
33
 
34
+ ## Tool and presentation SSE events
35
+
36
+ `POST /v1/responses` emits a tool name on `response.output_item.added`, with
37
+ `arguments` as a JSON string when available. `response.output_item.done` reports
38
+ how it ended. These `ok` / `error` / `blocked` statuses and optional `gate` are
39
+ Insika extensions, not OpenAI function-call lifecycle statuses.
40
+
41
+ ```text
42
+ event: response.output_item.added
43
+ data: {"type":"response.output_item.added","item":{"type":"function_call","name":"add_to_cart","call_id":"toolu_01","arguments":"{\"product_id\":\"SKU-1\"}"}}
44
+
45
+ event: response.output_item.done
46
+ data: {"type":"response.output_item.done","item":{"type":"function_call","name":"add_to_cart","call_id":"toolu_01","status":"blocked","gate":"provenance"}}
47
+ ```
48
+
49
+ `added` and `done` are two frames of **one** call, paired by `call_id` (the provider's
50
+ tool-call id, present whenever the provider gave one). A consumer that counts tool
51
+ calls counts `added` frames; `done` only says how each ended.
52
+
53
+ The completion frame does not contain the tool result body. Use stored session
54
+ traces for a per-call audit of what a tool returned.
55
+
56
+ A presentation call also emits a namespaced event:
57
+
58
+ ```text
59
+ event: insika.ui
60
+ data: {"type":"insika.ui","component":"product_cards","title":"Selected products","items":[{"type":"card","id":"SKU-1","url":"https://shop.example/products/1","caption":"Dark chocolate"}],"count":1,"dropped":[{"id":"SKU-2","reason":"no_card"}]}
61
+ ```
62
+
63
+ `count` is the number shown; `dropped` reasons are `unknown`, `no_card` or `max`.
64
+ An empty selection still emits the event with `count: 0`. Clients that render
65
+ cards consume this event; text-only clients can ignore it. See
66
+ [Presentation tools](TOOLS.md#presentation-tools-the-model-picks-ids-the-engine-shows-the-cards).
67
+
68
+ ## Seeding an eval conversation
69
+
70
+ Enable `evals.seeding` in platform settings on the eval deployment. For example,
71
+ an operator can `POST /v1/commands/update_settings` with
72
+ `{"patch":{"evals":{"seeding":true}}}`. Turn it off after the run.
73
+
74
+ `POST /v1/conversations/:id/seed` uses the same Bearer and tenant ID namespacing
75
+ as a turn. Its body is the snapshot itself, plus optional `customer`; do not wrap
76
+ it in `state`:
77
+
78
+ ```json
79
+ {
80
+ "customer": "eval-customer-1",
81
+ "evidence": {
82
+ "ids": ["SKU-1"],
83
+ "cards": [{ "type": "card", "id": "SKU-1", "url": "https://shop.example/1", "caption": "Dark chocolate" }]
84
+ },
85
+ "history": [
86
+ { "role": "assistant", "content": "The dark chocolate is SKU-1." }
87
+ ],
88
+ "memory": { "facts": { "preference": "dark chocolate" }, "notes": [] },
89
+ "briefing": { "fields": { "delivery_day": "Friday" } }
90
+ }
91
+ ```
92
+
93
+ It creates a missing session and returns `200` with `{ "session": … }`; seeding
94
+ does not run a model or create a task. Disabled seeding returns `403`, a session
95
+ with messages returns `409`, and invalid supported field shapes return `422`.
96
+ History accepts only `user` / `assistant` roles and is stamped `origin: engine`.
97
+
98
+ Memory uses `[tenant:]customer` when `customer` is supplied; subsequent turns
99
+ must carry the same customer. Without one, memory is written to the tenant cell,
100
+ or `chat:<session-id>` when there is no tenant. Use an isolated eval deployment:
101
+ a fresh conversation ID alone does not isolate tenant memory. Evidence IDs seed
102
+ only the runtime ledger, never model-visible text; `evidence.cards` seeds the cards
103
+ a presentation tool can show.
104
+ See [Snapshot evals](EVALS.md#state--a-case-starts-from-a-snapshot).
105
+
33
106
  ## The compatibility rule
34
107
 
35
108
  **Request and response shapes are stable; changes are additive-only.** A new
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/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,6 +23,7 @@ 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). |
@@ -106,10 +107,10 @@ budget first.
106
107
  With `memory` enabled, an agent gains a built-in `remember` tool for durable
107
108
  facts, and those facts (plus recent notes) are injected back into the prompt on
108
109
  later turns — **including turns in a different session**. Memory is scoped per
109
- agent, per `(tenant, customer)` when the message carries a `customer`, and per
110
- session otherwise a session's own memory lives in a marked `memory:chat:<session id>`
111
- cell, never a bare one, so the Customers drill cannot read a conversation as a
112
- 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
113
114
  conversation; memory is the small set of facts that should outlive any single
114
115
  conversation. Facts and notes are editable from the Studio agent page. See
115
116
  [`examples/memory/`](https://github.com/guizaols/insika/tree/main/examples/memory/) for a runnable cross-session example.
@@ -174,7 +175,7 @@ Attention is strongest at the end of the context: a goal stated only at the top
174
175
  is the first thing a 30-call turn forgets. So the recitation was **moved** there,
175
176
  not copied — the head never repeats it, and the turn pays for it once. It rides
176
177
  as a `user` message, like every other engine append inside a turn, so the system
177
- prefix stays byte-stable and the cache breakpoint at its end keeps hitting.
178
+ identity prefix stays byte-stable.
178
179
 
179
180
  The `still missing` list is the point: the *model* sees which declared fields are
180
181
  still unanswered, so it stops re-asking for something already given. Stored keys
@@ -194,17 +195,21 @@ turns and resumes — a resumed conversation re-opens with the briefing intact.
194
195
 
195
196
  ## The provider prefix cache
196
197
 
197
- 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.
198
200
 
199
- - **Automatic server-side prefix cache.** Some providers prefix-cache a stable
200
- system prefix automatically, at no cost to configure. This works **only because**
201
- the engine renders the system in two layers (below) and the volatile half sits
202
- **under** the identity boundary, keeping the cacheable prefix byte-stable.
203
- Anything that injects volatile content high in the system block breaks the cache.
204
- - **Manual cache breakpoints (opt-in).** With `prompt_caching` on **and** a
205
- provider that supports explicit cache control, the builder sets one cache
206
- breakpoint at the end of the system block. Only enable this for a byte-stable
207
- 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.
208
213
 
209
214
  Cache accounting surfaces as `cached_tokens` (reads) and `cache_creation_tokens`
210
215
  (writes), visible in telemetry and the Studio tokens chip.
@@ -215,13 +220,14 @@ The system block is partitioned into two cache layers:
215
220
 
216
221
  - **Identity** — bytes that change only on deploy/config edit: the persona
217
222
  prompt (`Prompt`), the level-1 skill list (`Skill`) and the deferred-tool
218
- 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.
219
225
  - **Volatile** — bytes that may change per turn: memory, session history,
220
226
  triggered skill bodies, the `<request_context>`. Everything else.
221
227
 
222
228
  The layer is a **provider-class contract**, not profile data: `ContextProvider`
223
229
  declares `def layer = :volatile` (conservative — nothing gets pinned by
224
- accident) and the three identity builtins override to `:identity`. A pack does
230
+ accident) and the identity providers override to `:identity`. A pack does
225
231
  not set it — a pack reorganizes *which content goes into the Prompt provider vs
226
232
  the volatile providers*. The Builder stamps the layer on every fragment at
227
233
  production, and the render order is **identity first, volatile after** — a
@@ -236,13 +242,14 @@ output must be byte-stable across turns).
236
242
 
237
243
  ### The observable cache: fingerprints and the invalidation reason
238
244
 
239
- Each turn, the Executor hashes the rendered prefix into a PII-free fingerprint
240
- chain — one SHA-256 per system category in render order, one for the tool
241
- schemas, one cumulative `prefix` and compares it against the previous turn's
242
- entry. The **invalidation reason** is the first category whose bytes changed (or
243
- vanished); a turn whose prefix held reports nothing. History is deliberately
244
- excluded: a new user message is a divergence every turn, which would be noise,
245
- 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.
246
253
 
247
254
  The Studio surfaces it in two places: the **session Context card** shows the
248
255
  turn's cache-hit percentage and the `broke: <category>` line (plus the
@@ -251,9 +258,8 @@ cache tab with the per-agent hit series over time. The per-agent series lives
251
258
  in its own capped store, because a session does not stamp its author — the
252
259
  per-session trace cannot answer "cache-hit over time for *this* agent".
253
260
 
254
- With the prefix stable by construction, the existing `prompt_caching` breakpoint
255
- sits on bytes that stay put the first (write) turn of a deployment pays the
256
- 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.
257
263
 
258
264
  ## The volume
259
265