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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +191 -0
- data/README.md +9 -6
- data/bin/insika +44 -3
- data/docs/AGENTS.md +74 -14
- data/docs/API.md +73 -0
- data/docs/ARCHITECTURE.md +45 -44
- data/docs/ARTIFACTS.md +42 -0
- data/docs/CHANNELS.md +19 -2
- data/docs/CONTEXT.md +86 -38
- data/docs/DEPLOY.md +27 -7
- data/docs/EVALS.md +98 -8
- data/docs/FACTS.md +4 -0
- data/docs/KNOWLEDGE.md +7 -0
- data/docs/LOADTEST.md +15 -27
- data/docs/MEDIA.md +1 -1
- data/docs/OBSERVABILITY.md +48 -4
- data/docs/POLICY.md +14 -5
- data/docs/RELEASING.md +4 -0
- data/docs/RUNNING-LOCAL.md +2 -2
- data/docs/SECURITY.md +28 -2
- data/docs/SOAK.md +1 -1
- data/docs/TOOLS.md +150 -32
- data/docs/prompts/ADD-TOOL.md +12 -2
- data/docs/prompts/DIAGNOSE-TURN.md +3 -0
- data/docs/prompts/GO-LIVE.md +6 -4
- data/lib/insika/agent_profile.rb +47 -10
- data/lib/insika/channels/web/widget.js +33 -0
- data/lib/insika/channels/web.rb +5 -2
- data/lib/insika/chat_builder.rb +90 -37
- data/lib/insika/commands/agent_payload.rb +1 -1
- data/lib/insika/commands/run_distillation.rb +5 -8
- data/lib/insika/commands/seed_session.rb +118 -0
- data/lib/insika/compaction.rb +196 -0
- data/lib/insika/context/builder.rb +35 -11
- data/lib/insika/context/fragment.rb +4 -1
- data/lib/insika/context/priority.rb +8 -0
- data/lib/insika/context/provider.rb +5 -0
- data/lib/insika/context/providers/briefing.rb +61 -29
- data/lib/insika/context/providers/fence_notice.rb +27 -0
- data/lib/insika/context/providers/knowledge.rb +7 -4
- data/lib/insika/context/providers/memory.rb +8 -4
- data/lib/insika/context/providers/session.rb +50 -10
- data/lib/insika/context_trace_store.rb +11 -1
- data/lib/insika/doctor.rb +213 -10
- data/lib/insika/dsl/runtime.rb +5 -0
- data/lib/insika/dsl.rb +6 -0
- data/lib/insika/edge_limiter.rb +4 -1
- data/lib/insika/env_schema.rb +5 -6
- data/lib/insika/errors.rb +1 -0
- data/lib/insika/evals/assertions.rb +92 -6
- data/lib/insika/evals/golden.rb +91 -2
- data/lib/insika/evals/runner.rb +20 -0
- data/lib/insika/evals/simulator.rb +11 -2
- data/lib/insika/evals/transport.rb +118 -16
- data/lib/insika/evidence.rb +79 -12
- data/lib/insika/executor.rb +94 -23
- data/lib/insika/fence.rb +96 -0
- data/lib/insika/golden_store.rb +3 -0
- data/lib/insika/loop_detector.rb +5 -34
- data/lib/insika/mcp_store.rb +5 -2
- data/lib/insika/mcp_tool_registry.rb +8 -1
- data/lib/insika/memory_store.rb +12 -0
- data/lib/insika/overlay_tool_registry.rb +5 -0
- data/lib/insika/prefix_fingerprint.rb +32 -27
- data/lib/insika/profile_source.rb +8 -0
- data/lib/insika/server/app.rb +43 -1
- data/lib/insika/server/rack_app.rb +2 -0
- data/lib/insika/server/responses.rb +35 -8
- data/lib/insika/session_store.rb +38 -5
- data/lib/insika/settings_store.rb +18 -2
- data/lib/insika/soak/runner.rb +4 -4
- data/lib/insika/spoken_transcript.rb +31 -0
- data/lib/insika/studio/app.rb +34 -8
- data/lib/insika/studio/forms.rb +29 -3
- data/lib/insika/studio/views/_agent_tab_config.erb +5 -1
- data/lib/insika/studio/views/session.erb +1 -1
- data/lib/insika/studio/views/settings.erb +11 -0
- data/lib/insika/studio/views/tool_edit.erb +6 -2
- data/lib/insika/telemetry/recorder.rb +61 -1
- data/lib/insika/templates/daily-digest/README.md +9 -0
- data/lib/insika/templates/research-analyst/agent.rb +10 -0
- data/lib/insika/tool_assembly.rb +21 -13
- data/lib/insika/tool_batch.rb +67 -0
- data/lib/insika/tool_definition.rb +73 -10
- data/lib/insika/tool_envelope.rb +102 -2
- data/lib/insika/tool_store.rb +9 -4
- data/lib/insika/tool_trace_store.rb +1 -1
- data/lib/insika/tool_usage_report.rb +172 -0
- data/lib/insika/tools/data_defined_tool.rb +1 -0
- data/lib/insika/tools/present.rb +122 -0
- data/lib/insika/tools/run_persona_eval.rb +6 -1
- data/lib/insika/tools/tool_search.rb +4 -2
- data/lib/insika/turn_budget.rb +91 -0
- data/lib/insika/turn_state.rb +13 -1
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +7 -0
- data/lib/insika/wiring/graph_chat.rb +4 -0
- data/lib/insika.rb +11 -0
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ec89eb1dae94e5a57733d0dd88a19d92ff98eb7e2ae0b907792e2a4ce8d508aa
|
|
4
|
+
data.tar.gz: cb882c8015e00c0c905e5945a05a5a465c938dedda762914c3737a64a9f0d4fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e2acb07f2ae896ee762cd3f1113dca3d462f80ea0bb32caea69f5db3fd6fbd31821cf9a5140b32c64c26b702b29b78aaf9fec0ca8f59653178dcb37afe9eca38
|
|
7
|
+
data.tar.gz: f8c98a2b76c802a75e18a77e0762ef42c650aa956afb3f131e1e9ccacfb2a56de87540b7389c740021008b45a174fa371d7a6dbb0a5a7cf83f248ce41b696770
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,197 @@ 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
|
+
|
|
86
|
+
## [0.8.0] - 2026-08-31
|
|
87
|
+
|
|
88
|
+
The post-consolidation hardening wave: the last two OpenClaw-era names are gone
|
|
89
|
+
from the wire (**breaking** — `insika:<agent>` / `X-Insika-Agent` and
|
|
90
|
+
`INSIKA_GATEWAY_TOKEN`, no aliases), and the engine learned to manage its own
|
|
91
|
+
long conversations — in-session compaction (RFC-0044), a tool budget that warns
|
|
92
|
+
before it kills, goal recitation at the tail of the context, and a tool-usage
|
|
93
|
+
audit report. Update `/v1` consumers and the gateway-token variable together
|
|
94
|
+
with this upgrade.
|
|
95
|
+
|
|
96
|
+
### Added
|
|
97
|
+
|
|
98
|
+
- **In-session compaction (RFC-0044).** When a session's uncompacted
|
|
99
|
+
transcript grows past `compact_after` messages (default 40), everything but
|
|
100
|
+
the last `keep_last` (default 20) is summarized by a cheap model
|
|
101
|
+
(`compaction.model` → platform `utility_model`) into one
|
|
102
|
+
`<conversation_summary>` history fragment; the tail stays verbatim and the
|
|
103
|
+
boundary is stable between compactions, so the prompt cache holds after it.
|
|
104
|
+
Runs post-turn, off the critical path, best-effort; persisted on the session
|
|
105
|
+
record with a monotonic boundary. Opt-in via the long-reserved Settings
|
|
106
|
+
`compaction` hash (the Studio general form is back), `insika doctor` warns
|
|
107
|
+
when it is enabled with no model, and every compaction is observable:
|
|
108
|
+
`:context_compacted` event, `insika.context.compacted` counter, `{upto,
|
|
109
|
+
runs}` + its own category in the context trace. See
|
|
110
|
+
[Context](docs/CONTEXT.md).
|
|
111
|
+
|
|
112
|
+
- **`insika tools:report` — the tool audit.** The per-session trace never
|
|
113
|
+
aggregated, so "which tools does this agent carry and never use?" had no
|
|
114
|
+
answer. The new CLI report reads the stored traces per agent (tasks →
|
|
115
|
+
sessions → `tool_traces`) and flags allowlisted-but-never-called tools,
|
|
116
|
+
tools over 30% errors in the window, and tools not called in N days
|
|
117
|
+
(default 14). Read-only — the operator removes. `--agent`, `--days`,
|
|
118
|
+
`--json`. See [Tools](docs/TOOLS.md).
|
|
119
|
+
|
|
120
|
+
- **Two series promoted to OTEL metrics.** `insika.cache.hit_rate` (histogram,
|
|
121
|
+
`%` — cache reads over the billed prompt, per turn, same arithmetic as the
|
|
122
|
+
Studio's per-agent series) and `insika.tool.loop_intervened` (counter — the
|
|
123
|
+
loop detector's one-shot warning, labelled by tool). Both computed in the
|
|
124
|
+
telemetry bridge from events that already existed: zero cost when OTEL is
|
|
125
|
+
off, no engine change. See [Observability](docs/OBSERVABILITY.md).
|
|
126
|
+
|
|
127
|
+
- **The doctor warns on a prompt file that outgrew a prompt.** The
|
|
128
|
+
`prompt-files` sweep now WARNs (never errors) past ~6 000 estimated tokens
|
|
129
|
+
or 600 lines, with the actual counts — the LLM-generated merchant-pack
|
|
130
|
+
shape that costs 20%+ extra tokens per turn. Corrupted-wrapper detection
|
|
131
|
+
is unchanged.
|
|
132
|
+
|
|
133
|
+
- **The tool budget is announced before it kills.** `max_tool_calls` was
|
|
134
|
+
enforced but silent: the model met the ceiling only when the turn died with
|
|
135
|
+
`stage: :tool_limit`, having delivered nothing. At **10 / 5 / 2** calls
|
|
136
|
+
remaining the engine now appends a short, escalating notice at the next
|
|
137
|
+
tool-batch boundary and emits `:tool_budget_warned`. It is a `user` message,
|
|
138
|
+
never a system one, so the cacheable system prefix stays byte-stable.
|
|
139
|
+
Verified live (DeepSeek): a turn asking for 30 lookups on a budget of 20 used
|
|
140
|
+
to die at `tool_limit`; it now stops at 18 and answers with what it has. The
|
|
141
|
+
turn's counter, the notices and the abort are one object (`TurnBudget`), and
|
|
142
|
+
the batch-boundary arithmetic it shares with the loop detector moved into
|
|
143
|
+
`ToolBatch`.
|
|
144
|
+
|
|
145
|
+
### Changed
|
|
146
|
+
|
|
147
|
+
- **The briefing recites the goal at the TAIL of the context.** `<briefing>`
|
|
148
|
+
used to carry known facts, the still-missing list and the next step in one
|
|
149
|
+
`:system` block at the top. The goal half was **moved** (not copied) into a
|
|
150
|
+
new `<recitation>` block rendered after the whole history, as the last thing
|
|
151
|
+
the model reads before the current message — attention is strongest at the
|
|
152
|
+
end, and a goal stated only at the top is the first thing a 30-call turn
|
|
153
|
+
forgets. New `:tail` fragment placement; the head keeps the durable facts.
|
|
154
|
+
|
|
155
|
+
- **The `/v1/responses` wire names are insika-native**: the agent arrives as
|
|
156
|
+
`model: "insika:<agent>"` and the fallback header is `X-Insika-Agent`. Hard
|
|
157
|
+
rename, no alias — a consumer still sending `openclaw:<agent>` gets
|
|
158
|
+
"model/agent missing". Update every `/v1` consumer together with this upgrade.
|
|
159
|
+
- **`INSIKA_GATEWAY_TOKEN` replaces `OPENCLAW_GATEWAY_TOKEN`** as the Bearer for
|
|
160
|
+
`/v1` + `/a2a`. Hard rename, no alias: the old name was a leftover of the
|
|
161
|
+
OpenClaw migration and is now a foreign var the engine ignores. Update the
|
|
162
|
+
variable on every deployment before upgrading. The dead `OPENCLAW_AGENTS_DIR`
|
|
163
|
+
env spec (nothing read it) is gone.
|
|
164
|
+
|
|
165
|
+
### Fixed
|
|
166
|
+
|
|
167
|
+
- **The Studio home charts went blank for three hours every day.** The activity
|
|
168
|
+
buckets key on calendar parts (date, hour) of `updated_at`, which the engine
|
|
169
|
+
writes in UTC, but `render_home` read `Time.now` in the host's local zone. On a
|
|
170
|
+
UTC-3 host, from 21:00 local onward "today" was already tomorrow in UTC: the
|
|
171
|
+
14-day chart matched no session and the 24h sparkline's floor was set three
|
|
172
|
+
hours in the future, dropping the newest buckets. Stamps are now read through
|
|
173
|
+
one `utc_time` helper and `now` is UTC. Instant comparisons (`active_now`)
|
|
174
|
+
never had the bug — only the calendar arithmetic did.
|
|
175
|
+
- **The Conversations trend showed "−1" for the day's first conversation.** The
|
|
176
|
+
14-day series is oldest-first and ends at today, so its last pair reads
|
|
177
|
+
`[yesterday, today]`; it was destructured the other way round, inverting the
|
|
178
|
+
sign on every conversations delta. The Messages card was always correct.
|
|
179
|
+
- **Declaring a tool allow/deny list now opts the profile into the policy.** The
|
|
180
|
+
lists are applied by exactly one policy — the builtin `tool_allowlist` — and
|
|
181
|
+
the policy engine runs only the policies a profile *names*. So an agent with
|
|
182
|
+
`tools_allow: ["a", "b"]` and no `policies` sent **every registered tool** to
|
|
183
|
+
the model, silently: 508 tools / 52 405 tokens of schemas per request against
|
|
184
|
+
an allowlist of 2, observed on a real run. `tools_deny` was ignored the same
|
|
185
|
+
way. `AgentProfile.build` now appends `tool_allowlist` whenever `tools_allow`,
|
|
186
|
+
`tools_deny` or `tools_allow_groups` is declared — presence, not emptiness, so
|
|
187
|
+
`tools_allow: []` still means "no tools". A stored record left in the old
|
|
188
|
+
state is repaired on read but stays wrong on disk, so `doctor` gained a
|
|
189
|
+
`tool-allowlist` **error** naming each agent to re-save.
|
|
190
|
+
- **A DSL agent could write its briefing and never read it back.** The
|
|
191
|
+
`Insika.agent`/`Insika.system` runtime wired every context provider except
|
|
192
|
+
`Briefing`, so `update_briefing` / `set_next_step` persisted state that never
|
|
193
|
+
reached a prompt. Now wired, before `Session`, like `config/wiring.rb`.
|
|
194
|
+
- **Docker image shipped without `docs/`, so `GET /start.md` was a 500 with
|
|
195
|
+
`INSIKA_ONBOARDING=1`.** `.dockerignore` excluded the whole tree; the onboarding
|
|
196
|
+
surface reads `docs/onboarding/start.md`, `docs/prompts/` and the public
|
|
197
|
+
`docs/*.md` from disk at request time. Only the Jekyll build output and the
|
|
198
|
+
internal notes are excluded now, and a spec asserts `.dockerignore` never drops
|
|
199
|
+
a file `Insika::Onboarding` serves. Found on the first Railway deploy with
|
|
200
|
+
onboarding on; the gem was never affected (its payload keeps `docs/`).
|
|
201
|
+
|
|
11
202
|
## [0.7.0] - 2026-08-29
|
|
12
203
|
|
|
13
204
|
The pre-1.0 consolidation: MCP servers as first-class tools, a template gallery,
|
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
|
|
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
|
|
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,10 +142,10 @@ navigates by.
|
|
|
140
142
|
|
|
141
143
|
**Improve**
|
|
142
144
|
|
|
143
|
-
- [Evals](docs/EVALS.md) —
|
|
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
|
-
- [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
|
|
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.
|
|
147
149
|
- [Facts](docs/FACTS.md) — distill finished customer conversations into proposed facts; a human approves them into memory (nothing is ever applied automatically).
|
|
148
150
|
- [Harvest](docs/HARVEST.md) — mine real traffic for SKILL proposals; the negative list, the evidence-ledger grounding filter and the double gate hold every proposal to a human approval and the append-only log (nothing is ever applied automatically).
|
|
149
151
|
|
|
@@ -176,5 +178,6 @@ and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). Vulnerabilities go through
|
|
|
176
178
|
|
|
177
179
|
## Status
|
|
178
180
|
|
|
179
|
-
Pre-
|
|
180
|
-
([CHANGELOG.md](CHANGELOG.md)). Licensed
|
|
181
|
+
Pre-1.0 — published on [RubyGems](https://rubygems.org/gems/insika); APIs may
|
|
182
|
+
still change between minor versions ([CHANGELOG.md](CHANGELOG.md)). Licensed
|
|
183
|
+
**MIT** ([LICENSE](LICENSE)).
|
data/bin/insika
CHANGED
|
@@ -32,6 +32,7 @@ module Insika
|
|
|
32
32
|
when "harvest" then harvest(argv)
|
|
33
33
|
when "harvest:negative" then harvest_negative_import(argv)
|
|
34
34
|
when "harvest:criterion" then harvest_criterion_check(argv)
|
|
35
|
+
when "tools:report" then tools_report(argv)
|
|
35
36
|
when "knowledge:backfill" then knowledge_backfill(argv)
|
|
36
37
|
when "knowledge:export" then knowledge_export(argv)
|
|
37
38
|
when "soak" then exit(Insika::Soak::Runner.main(argv, stdout: $stdout, stderr: $stderr))
|
|
@@ -65,6 +66,11 @@ module Insika
|
|
|
65
66
|
seed the profile's negative list from a rules file
|
|
66
67
|
harvest:criterion check --file F
|
|
67
68
|
strict-load the frozen conversion criterion
|
|
69
|
+
tools:report [--agent ID] [--days N] [--json]
|
|
70
|
+
tool usage per agent over the stored traces:
|
|
71
|
+
allowlisted-but-never-called, error rate > 30%
|
|
72
|
+
in the window, and stale (not called in N days).
|
|
73
|
+
Read-only — the operator removes
|
|
68
74
|
knowledge:backfill --agent ID [--since ISO8601] [--json]
|
|
69
75
|
the recovery path for post-turn extraction: replays
|
|
70
76
|
the agent's stored sessions through the same
|
|
@@ -337,7 +343,7 @@ def evals_simulate(argv)
|
|
|
337
343
|
require "fileutils"
|
|
338
344
|
require "time"
|
|
339
345
|
opts = { base_url: ENV["INSIKA_URL"] || "http://localhost:9292",
|
|
340
|
-
token: ENV["
|
|
346
|
+
token: ENV["INSIKA_GATEWAY_TOKEN"] || ENV["ADMIN_TOKEN"] || "local-demo",
|
|
341
347
|
timeout: 120, persona_model: ENV["EVAL_PERSONA_MODEL"],
|
|
342
348
|
judge_model: ENV["EVAL_JUDGE_MODEL"], judge_provider: ENV["EVAL_JUDGE_PROVIDER"] }
|
|
343
349
|
OptionParser.new do |o|
|
|
@@ -345,7 +351,7 @@ def evals_simulate(argv)
|
|
|
345
351
|
o.on("--persona FILE", "the persona YAML — a case with a `persona:` key (required)") { |v| opts[:persona] = v }
|
|
346
352
|
o.on("--target T", "agent id (over --base-url) or an A2A http(s) URL (required)") { |v| opts[:target] = v }
|
|
347
353
|
o.on("--base-url URL", "deployment base URL (default #{opts[:base_url]})") { |v| opts[:base_url] = v }
|
|
348
|
-
o.on("--token T", "gateway token (default
|
|
354
|
+
o.on("--token T", "gateway token (default INSIKA_GATEWAY_TOKEN/local-demo)") { |v| opts[:token] = v }
|
|
349
355
|
o.on("--timeout N", Integer, "per-turn read timeout, and the A2A poll budget in seconds (default 120)") { |v| opts[:timeout] = v }
|
|
350
356
|
o.on("--staging", "the target is a staging deployment (side effects acceptable)") { opts[:staging] = true }
|
|
351
357
|
o.on("--eval-profile", "the target runs an eval profile (side-effect tools swapped for fakes)") { opts[:eval_profile] = true }
|
|
@@ -384,7 +390,7 @@ def evals_simulate(argv)
|
|
|
384
390
|
# paired comparison, which it silently invalidates.
|
|
385
391
|
require "securerandom"
|
|
386
392
|
conv = opts[:conv] || "sim-#{golden.id}-#{SecureRandom.hex(4)}"
|
|
387
|
-
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)
|
|
388
394
|
|
|
389
395
|
verdict = judge && judge.score_conversation(
|
|
390
396
|
rubric: golden.rubric, transcript: run.transcript, policy: golden.policy,
|
|
@@ -1162,6 +1168,41 @@ end
|
|
|
1162
1168
|
"last #{n} session(s)"
|
|
1163
1169
|
end
|
|
1164
1170
|
|
|
1171
|
+
# insika tools:report [--agent ID] [--days N] [--json]
|
|
1172
|
+
#
|
|
1173
|
+
# The tool audit: reads the SAME durable backend as `doctor` (tasks →
|
|
1174
|
+
# sessions → tool_traces), no app boot. Read-only — it names never-called
|
|
1175
|
+
# allowlisted tools, high-error tools and stale tools; the operator removes.
|
|
1176
|
+
def tools_report(argv)
|
|
1177
|
+
opts = { json: false, days: Insika::ToolUsageReport::WINDOW_DAYS }
|
|
1178
|
+
OptionParser.new do |o|
|
|
1179
|
+
o.banner = "Usage: insika tools:report [--agent ID] [--days N] [--json]"
|
|
1180
|
+
o.on("--agent ID", "narrow the report to one stored agent") { |v| opts[:agent] = v }
|
|
1181
|
+
o.on("--days N", Integer, "window for error-rate and staleness (default #{opts[:days]})") { |v| opts[:days] = v }
|
|
1182
|
+
o.on("--json", "emit the report as JSON") { opts[:json] = true }
|
|
1183
|
+
end.parse!(argv)
|
|
1184
|
+
|
|
1185
|
+
backend = Insika::Wiring::Graph.backend_from_env
|
|
1186
|
+
config_store = Insika::ConfigStore.new(store: backend)
|
|
1187
|
+
report = Insika::ToolUsageReport.new(
|
|
1188
|
+
task_store: Insika::TaskStore.new(store: backend),
|
|
1189
|
+
tool_trace_store: Insika::ToolTraceStore.new(store: backend),
|
|
1190
|
+
profile_source: Insika::StoredProfileSource.new(config_store: config_store)
|
|
1191
|
+
).generate(days: opts[:days], agent: opts[:agent])
|
|
1192
|
+
|
|
1193
|
+
if opts[:agent] && report.agents.empty?
|
|
1194
|
+
warn "insika tools:report: no stored agent '#{opts[:agent]}'"
|
|
1195
|
+
exit 2
|
|
1196
|
+
end
|
|
1197
|
+
|
|
1198
|
+
if opts[:json]
|
|
1199
|
+
require "json"
|
|
1200
|
+
puts JSON.pretty_generate(report.to_h)
|
|
1201
|
+
else
|
|
1202
|
+
puts report.to_s
|
|
1203
|
+
end
|
|
1204
|
+
end
|
|
1205
|
+
|
|
1165
1206
|
# Config stores over the real durable backend — no app boot / no DEEPSEEK.
|
|
1166
1207
|
def build_doctor
|
|
1167
1208
|
backend = Insika::Wiring::Graph.backend_from_env
|
data/docs/AGENTS.md
CHANGED
|
@@ -32,9 +32,10 @@ All three land on the **same** config-over-code import path — they differ only
|
|
|
32
32
|
ergonomics, not in what they produce.
|
|
33
33
|
|
|
34
34
|
- **DSL** — `Insika.agent("id") { … }` builds an agent definition and imports it
|
|
35
|
-
into the durable store. Best for code-defined agents and examples. The DSL
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
into the durable store. Best for code-defined agents and examples. The DSL also
|
|
36
|
+
names the **skill**-allowlist policy for you (the tool one is implied by any
|
|
37
|
+
declared tool list, on every path). `model` is optional — a nil model resolves
|
|
38
|
+
the platform `default_model` at turn start.
|
|
38
39
|
- **API** — `POST /v1/agents` with a definition (a "pack": an agent config plus its
|
|
39
40
|
prompt files, skills, and data-tools). The import is **idempotent** and
|
|
40
41
|
**authoritative** — what leaves the definition leaves the agent, so a
|
|
@@ -108,6 +109,14 @@ DEFAULT_LIMITS = {
|
|
|
108
109
|
|
|
109
110
|
`build` merges your overrides over these — you set only the deltas.
|
|
110
111
|
|
|
112
|
+
`max_tool_calls` is announced before it kills. At **10 / 5 / 2** calls remaining
|
|
113
|
+
the engine appends a short user message at the next tool-batch boundary
|
|
114
|
+
("2 tool calls left — consolidate what you already have and answer now"), so a
|
|
115
|
+
long turn converges on an answer instead of dying with `stage: :tool_limit` and
|
|
116
|
+
delivering nothing. It is a `user` message, never a system one: the system prefix
|
|
117
|
+
stays byte-stable and `prompt_caching` keeps hitting. Each threshold fires at
|
|
118
|
+
most once per turn and emits `:tool_budget_warned` on the event stream.
|
|
119
|
+
|
|
111
120
|
`max_tool_repeat` is the loop guard: the same tool called with **identical
|
|
112
121
|
arguments** that many times in a row gets ONE in-turn warning (a user message at
|
|
113
122
|
the next tool-batch boundary: "you already ran this, answer with what you
|
|
@@ -135,9 +144,44 @@ the thing worth declaring:
|
|
|
135
144
|
tool_persistence false # remove the block for this agent
|
|
136
145
|
```
|
|
137
146
|
|
|
138
|
-
The block is
|
|
139
|
-
|
|
140
|
-
|
|
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.
|
|
141
185
|
|
|
142
186
|
### Why some limits are missing from that list
|
|
143
187
|
|
|
@@ -164,17 +208,16 @@ So the two groups read differently on purpose:
|
|
|
164
208
|
### `tool_concurrency` — parallel tool calls
|
|
165
209
|
|
|
166
210
|
When the model asks for several tools in one step, they run **one at a time by
|
|
167
|
-
default**. Raise `tool_concurrency`
|
|
168
|
-
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:
|
|
169
213
|
|
|
170
214
|
```ruby
|
|
171
215
|
limit :tool_concurrency, 4 # nil / 0 / 1 = serial (the default); N = at most N at once
|
|
172
216
|
```
|
|
173
217
|
|
|
174
218
|
One number is both the switch and the cap. It pays off only when a turn issues
|
|
175
|
-
several **slow, independent**
|
|
176
|
-
|
|
177
|
-
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
|
|
178
221
|
optional: an uncapped batch of 15 data tools is 15 simultaneous requests to the
|
|
179
222
|
same backend.
|
|
180
223
|
|
|
@@ -382,13 +425,23 @@ The same three-state rule governs tools, skills, context providers, and workflow
|
|
|
382
425
|
For tools, a paired deny list (`tools_deny`) **always wins**, and
|
|
383
426
|
`tools_allow_groups` unions a per-group allowlist on top of `tools_allow`.
|
|
384
427
|
|
|
428
|
+
**Declaring a list is opting into it.** The lists are applied by one policy, the
|
|
429
|
+
builtin `tool_allowlist`, and the policy engine runs only the policies a profile
|
|
430
|
+
names — so declaring `tools_allow` without naming that policy used to mean the
|
|
431
|
+
model got *every* registered tool. The engine now adds `tool_allowlist` for you
|
|
432
|
+
the moment any of `tools_allow`, `tools_deny` or `tools_allow_groups` is
|
|
433
|
+
declared, on every creation path. Presence is the trigger, not content:
|
|
434
|
+
`tools_allow: []` means "no tools" and enforces just as hard. If a stored agent
|
|
435
|
+
predates this, `insika doctor` names it — the engine repairs it on read, but the
|
|
436
|
+
stored record stays wrong until you re-save it.
|
|
437
|
+
|
|
385
438
|
Three capabilities invert the default — `nil`/absent means **OFF**, not "all":
|
|
386
439
|
`subagents`, `memory`, and `guardrails` (each defaults to off or a conservative
|
|
387
440
|
setting, never "everything on"). `tool_output_compression` is a fourth: opt-in
|
|
388
441
|
mechanical dedupe of repeated tool results in the history (see
|
|
389
|
-
[Context](CONTEXT.md#
|
|
390
|
-
off by default because it changes what the model sees
|
|
391
|
-
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
|
|
392
445
|
[`tool_persistence`](#tool_persistence--dont-give-up-on-the-first-empty-result)).
|
|
393
446
|
|
|
394
447
|
### Declaring what this deployment has
|
|
@@ -673,6 +726,13 @@ optionally shared outside the Studio via an expiring signed link
|
|
|
673
726
|
saving agent — never a parameter the model types. Artifact content is LLM
|
|
674
727
|
output and is served as untrusted. See [Artifacts](ARTIFACTS.md).
|
|
675
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
|
+
|
|
676
736
|
## See also
|
|
677
737
|
|
|
678
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
|