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