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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c2b2fbe064af45aa132f93de00954b24dc504e052db2e059b4f6daa151e248b
4
- data.tar.gz: 50cabcc10d82eabdfd08de562bd61feddf1a9de68ee6427343c80c5cf6ad9d8c
3
+ metadata.gz: ec89eb1dae94e5a57733d0dd88a19d92ff98eb7e2ae0b907792e2a4ce8d508aa
4
+ data.tar.gz: cb882c8015e00c0c905e5945a05a5a465c938dedda762914c3737a64a9f0d4fb
5
5
  SHA512:
6
- metadata.gz: 3004e30a234349bcb91db185fcc31f9b52cbdeb7660c613b1fc229adf6bbbe717d9abc8ebcdc5bf9ecc125986694c0d2f3a82e1d48eb83105ff74db0467e4d91
7
- data.tar.gz: ca4bec6a8b8cd03ee1a5c2df627e7da778d3dd157f30f848c70f8077ac467570284bf934e3a4c8040fb223503855d9cdcad111b9139ed093c702b6a8850ea1ff
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 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,10 +142,10 @@ 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
- - [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 (retrieval and consolidation are not shipped yet).
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-releaseAPIs may still change, nothing is tagged yet
180
- ([CHANGELOG.md](CHANGELOG.md)). Licensed **MIT** ([LICENSE](LICENSE)).
181
+ Pre-1.0published 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["OPENCLAW_GATEWAY_TOKEN"] || ENV["ADMIN_TOKEN"] || "local-demo",
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 OPENCLAW_GATEWAY_TOKEN/local-demo)") { |v| opts[:token] = v }
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
- auto-enables the tool- and skill-allowlist policies. `model` is optional a nil
37
- model resolves the platform `default_model` at turn start.
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 a byte-stable constant, so `prompt_caching` stays effective: the
139
- deploy that introduces it costs one cache write per agent, and every turn after
140
- 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.
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` and they run together, capped at that number
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** calls (data tools waiting on HTTP) the wall-clock
176
- becomes the slowest call instead of the sum. It buys nothing for fast in-process
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#compaction-is-not-wired--except-the-mechanical-dedupe)),
390
- off by default because it changes what the model sees. And one flag inverts the
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