insika 0.3.0 → 0.8.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 +296 -0
- data/README.md +48 -12
- data/bin/insika +725 -0
- data/bin/insika-router +87 -0
- data/docs/AGENTS.md +116 -406
- data/docs/API.md +5 -5
- data/docs/ARCHITECTURE.md +3 -2
- data/docs/ARTIFACTS.md +137 -0
- data/docs/BENCHMARK.md +2 -2
- data/docs/CHANNELS.md +14 -14
- data/docs/CONTEXT.md +63 -19
- data/docs/DEMO.md +80 -0
- data/docs/DEPLOY.md +87 -10
- data/docs/EMBEDDING.md +1 -1
- data/docs/EVALS.md +128 -3
- data/docs/FACTS.md +3 -3
- data/docs/HARVEST.md +5 -6
- data/docs/KNOWLEDGE.md +290 -0
- data/docs/LOADTEST.md +17 -29
- data/docs/MEDIA.md +128 -0
- data/docs/OBSERVABILITY.md +46 -12
- data/docs/OUTCOMES.md +137 -0
- data/docs/PLUGINS.md +51 -6
- data/docs/POLICY.md +222 -0
- data/docs/REFINEMENT.md +14 -9
- data/docs/RELEASING.md +4 -4
- data/docs/ROUTER.md +213 -0
- data/docs/RUNNING-LOCAL.md +5 -5
- data/docs/SCHEDULING.md +121 -0
- data/docs/SECURITY.md +23 -7
- data/docs/SKILLS.md +11 -2
- data/docs/SOAK.md +3 -3
- data/docs/TEMPLATES.md +134 -0
- data/docs/TOOLS.md +176 -27
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/_includes/head_custom.html +5 -0
- data/docs/_includes/title.html +13 -0
- data/docs/_sass/color_schemes/insika.scss +32 -0
- data/docs/_sass/custom/custom.scss +199 -0
- data/docs/_sass/custom/setup.scss +26 -0
- data/docs/assets/img/favicon.svg +7 -0
- data/docs/assets/img/insika-mark.svg +7 -0
- data/docs/core-concepts.md +21 -0
- data/docs/domain.md +4 -4
- data/docs/improve.md +20 -0
- data/docs/index.md +8 -5
- data/docs/integrate.md +20 -0
- data/docs/operate.md +13 -6
- data/docs/prompts/ADD-TOOL.md +118 -0
- data/docs/prompts/DIAGNOSE-TURN.md +65 -0
- data/docs/prompts/GO-LIVE.md +138 -0
- data/docs/prompts/RUN-EXAMPLES.md +70 -0
- data/docs/reference.md +19 -0
- data/docs/ship.md +10 -2
- data/docs/start-here.md +18 -0
- data/lib/insika/agent_profile.rb +99 -17
- data/lib/insika/artifact_signing.rb +82 -0
- data/lib/insika/artifact_store.rb +160 -0
- data/lib/insika/channel_delivery.rb +1 -1
- data/lib/insika/chat_builder.rb +50 -19
- data/lib/insika/commands/agent_payload.rb +2 -2
- data/lib/insika/commands/backfill_knowledge.rb +145 -0
- data/lib/insika/commands/delete_artifact.rb +35 -0
- data/lib/insika/commands/delete_concept.rb +34 -0
- data/lib/insika/commands/delete_mcp.rb +6 -2
- data/lib/insika/commands/delete_tenant_data.rb +15 -3
- data/lib/insika/commands/gate_refinement.rb +1 -1
- data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
- data/lib/insika/commands/restore_concept.rb +34 -0
- data/lib/insika/commands/seed_demo_data.rb +31 -0
- data/lib/insika/commands/upsert_mcp.rb +6 -3
- data/lib/insika/commands/write_concept.rb +57 -0
- data/lib/insika/compaction.rb +196 -0
- data/lib/insika/context/builder.rb +6 -2
- data/lib/insika/context/fragment.rb +4 -1
- data/lib/insika/context/priority.rb +8 -0
- data/lib/insika/context/providers/briefing.rb +53 -24
- data/lib/insika/context/providers/knowledge.rb +108 -0
- data/lib/insika/context/providers/prompt.rb +30 -24
- data/lib/insika/context/providers/session.rb +46 -10
- data/lib/insika/context_trace_store.rb +11 -1
- data/lib/insika/cron.rb +189 -0
- data/lib/insika/demo/agent_attrs.rb +43 -0
- data/lib/insika/demo/golden_cases.rb +81 -0
- data/lib/insika/demo/seeder.rb +336 -0
- data/lib/insika/doctor.rb +280 -17
- data/lib/insika/dsl/definition.rb +3 -2
- data/lib/insika/dsl/runtime.rb +64 -79
- data/lib/insika/dsl/server_boot.rb +23 -1
- data/lib/insika/dsl/system.rb +10 -2
- data/lib/insika/dsl.rb +103 -2
- data/lib/insika/env_schema.rb +21 -7
- data/lib/insika/evals/golden.rb +41 -4
- data/lib/insika/evals/judge.rb +47 -2
- data/lib/insika/evals/pairwise.rb +11 -0
- data/lib/insika/evals/persona.rb +98 -0
- data/lib/insika/evals/runner.rb +9 -0
- data/lib/insika/evals/simulator.rb +225 -0
- data/lib/insika/evals/transport.rb +84 -2
- data/lib/insika/event_stream.rb +10 -0
- data/lib/insika/executor.rb +295 -55
- data/lib/insika/followup_policy.rb +2 -25
- data/lib/insika/golden_store.rb +16 -1
- data/lib/insika/grounding/matcher.rb +1 -1
- data/lib/insika/knowledge.rb +680 -0
- data/lib/insika/knowledge_store.rb +140 -0
- data/lib/insika/loop_detector.rb +5 -34
- data/lib/insika/mcp_client.rb +94 -0
- data/lib/insika/mcp_json.rb +74 -0
- data/lib/insika/mcp_live_tool.rb +43 -0
- data/lib/insika/mcp_store.rb +98 -26
- data/lib/insika/mcp_tool_ingestor.rb +30 -8
- data/lib/insika/mcp_tool_registry.rb +100 -0
- data/lib/insika/media.rb +115 -31
- data/lib/insika/message_origin.rb +1 -1
- data/lib/insika/middleware.rb +9 -0
- data/lib/insika/onboarding.rb +17 -1
- data/lib/insika/outcome_store.rb +1 -1
- data/lib/insika/overlay_tool_registry.rb +37 -17
- data/lib/insika/packaging.rb +2 -2
- data/lib/insika/profile_source.rb +15 -1
- data/lib/insika/prompt_catalog.rb +10 -0
- data/lib/insika/retention.rb +36 -1
- data/lib/insika/router/app.rb +157 -0
- data/lib/insika/router/backend_pool.rb +98 -0
- data/lib/insika/router/hash_ring.rb +55 -0
- data/lib/insika/router/proxy_body.rb +34 -0
- data/lib/insika/router/session_key.rb +54 -0
- data/lib/insika/router.rb +18 -0
- data/lib/insika/schedule.rb +177 -0
- data/lib/insika/schedule_engine.rb +314 -0
- data/lib/insika/schedule_store.rb +208 -0
- data/lib/insika/server/app.rb +105 -15
- data/lib/insika/server/rack_app.rb +5 -1
- data/lib/insika/server/responses.rb +5 -5
- data/lib/insika/session_store.rb +34 -4
- data/lib/insika/settings_store.rb +8 -1
- data/lib/insika/skill_catalog.rb +12 -0
- data/lib/insika/soak/runner.rb +4 -4
- data/lib/insika/steer_injector.rb +21 -10
- data/lib/insika/studio/app.rb +591 -47
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +21 -21
- data/lib/insika/studio/forms.rb +57 -5
- data/lib/insika/studio/nav_icons.rb +14 -1
- data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
- data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
- data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
- data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
- data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
- data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
- data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
- data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
- data/lib/insika/studio/views/_agents_master.erb +44 -0
- data/lib/insika/studio/views/_message.erb +49 -32
- data/lib/insika/studio/views/agent_detail.erb +61 -820
- data/lib/insika/studio/views/agents.erb +70 -57
- data/lib/insika/studio/views/artifact.erb +23 -0
- data/lib/insika/studio/views/artifacts.erb +59 -0
- data/lib/insika/studio/views/evals.erb +2 -2
- data/lib/insika/studio/views/facts.erb +1 -1
- data/lib/insika/studio/views/funnel.erb +1 -1
- data/lib/insika/studio/views/home.erb +106 -67
- data/lib/insika/studio/views/knowledge.erb +123 -0
- data/lib/insika/studio/views/layout.erb +14 -11
- data/lib/insika/studio/views/mcp.erb +174 -80
- data/lib/insika/studio/views/session.erb +231 -177
- data/lib/insika/studio/views/settings.erb +50 -1
- data/lib/insika/studio/views/skills.erb +1 -1
- data/lib/insika/studio/views/tools.erb +24 -9
- data/lib/insika/telemetry/recorder.rb +49 -1
- data/lib/insika/templates/browser-agent/README.md +36 -0
- data/lib/insika/templates/browser-agent/agent.rb +49 -0
- data/lib/insika/templates/daily-digest/README.md +47 -0
- data/lib/insika/templates/daily-digest/agent.rb +77 -0
- data/lib/insika/templates/repo-explorer/README.md +36 -0
- data/lib/insika/templates/repo-explorer/agent.rb +45 -0
- data/lib/insika/templates/research-analyst/README.md +26 -0
- data/lib/insika/templates/research-analyst/agent.rb +68 -0
- data/lib/insika/templates/review-panel/README.md +20 -0
- data/lib/insika/templates/review-panel/agent.rb +50 -0
- data/lib/insika/templates/travel-planner/README.md +35 -0
- data/lib/insika/templates/travel-planner/agent.rb +87 -0
- data/lib/insika/templates.rb +112 -0
- data/lib/insika/tick.rb +24 -12
- data/lib/insika/timezone.rb +45 -0
- data/lib/insika/tool_batch.rb +67 -0
- data/lib/insika/tool_usage_report.rb +162 -0
- data/lib/insika/tools/generate_image.rb +52 -7
- data/lib/insika/tools/load_knowledge.rb +74 -0
- data/lib/insika/tools/run_persona_eval.rb +328 -0
- data/lib/insika/tools/save_artifact.rb +95 -0
- data/lib/insika/turn_budget.rb +91 -0
- data/lib/insika/turn_output.rb +1 -1
- data/lib/insika/turn_state.rb +15 -4
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +184 -12
- data/lib/insika/wiring/graph_chat.rb +102 -0
- data/lib/insika.rb +64 -0
- metadata +109 -5
- data/docs/build.md +0 -14
- data/docs/understand.md +0 -10
data/docs/KNOWLEDGE.md
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Knowledge
|
|
3
|
+
parent: Improve
|
|
4
|
+
nav_order: 4
|
|
5
|
+
permalink: /knowledge/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Knowledge — learning from conversations
|
|
9
|
+
|
|
10
|
+
An agent is amnesiac in exactly one dimension. It has history (the session),
|
|
11
|
+
memory (facts about one customer, written by the `remember` tool), skills
|
|
12
|
+
(procedures a human curated), and tool traces — but nothing turns a finished
|
|
13
|
+
conversation into knowledge the agent can use on someone else's turn. Every
|
|
14
|
+
conversation teaches a store things — which product customers actually ask
|
|
15
|
+
about, which objection keeps coming up, which CEP maps to which distribution
|
|
16
|
+
center — and today all of it is write-once trace data nobody reads back.
|
|
17
|
+
|
|
18
|
+
Knowledge is that loop. After a turn completes, the engine can extract durable
|
|
19
|
+
**concepts** from it — facts, procedures, policies, objections — and persist
|
|
20
|
+
them for the agent (never a customer, never a session) to build on.
|
|
21
|
+
|
|
22
|
+
**What ships today: extraction, consolidation, retrieval, export, and a
|
|
23
|
+
Studio page.** The engine writes what it learns, decides whether a repeat
|
|
24
|
+
sighting confirms, merges with, or contradicts what it already knew,
|
|
25
|
+
retrieves the concepts relevant to a turn's message back into the prompt,
|
|
26
|
+
and an operator can see, edit and resolve all of it in the Studio. Only the
|
|
27
|
+
optional FTS5 index remains, deferred with a measured trigger — see
|
|
28
|
+
[What's not here yet](#whats-not-here-yet).
|
|
29
|
+
|
|
30
|
+
## The concept format
|
|
31
|
+
|
|
32
|
+
One concept is one record — a markdown document with a YAML frontmatter
|
|
33
|
+
block, the same shape a `SKILL.md` uses:
|
|
34
|
+
|
|
35
|
+
```markdown
|
|
36
|
+
---
|
|
37
|
+
name: cep-sudeste-cd-campinas
|
|
38
|
+
description: CEPs 13xxx-13999 ship from the Campinas DC, 1-2 business days.
|
|
39
|
+
type: fact # fact | entity | procedure | policy | objection
|
|
40
|
+
provenance: observed # policy (curated) | observed (learned from conversations)
|
|
41
|
+
confidence: 0.6 # 0..1, evidence-weighted
|
|
42
|
+
sources: ["sess_8f3c"] # session ids, never message content
|
|
43
|
+
occurrences: 1
|
|
44
|
+
created_at: 2026-08-24T18:02:11Z
|
|
45
|
+
updated_at: 2026-08-24T18:02:11Z
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
Orders to CEP range 13000-13999 are fulfilled by the Campinas DC. Quoted
|
|
49
|
+
delivery is 1-2 business days. Related: [[frete-gratis-acima-199]].
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The model only ever writes `name`, `description`, `type` and `body`. Every
|
|
53
|
+
other field — `provenance`, `confidence`, `sources`, `occurrences`, the
|
|
54
|
+
timestamps — is stamped by the engine; an extraction answer that tries to
|
|
55
|
+
supply one of those is rejected outright (the same discipline Facts applies
|
|
56
|
+
to a model-authored scope). `[[links]]` inside the body are plain text stored
|
|
57
|
+
as-is; retrieval resolves them lazily by name at read time (below) — that one
|
|
58
|
+
hop is the entire "graph," never a stored structure of its own.
|
|
59
|
+
|
|
60
|
+
**`provenance` is not decoration.** Everything the extractor writes is
|
|
61
|
+
`provenance: observed` — a claim learned from what people said in
|
|
62
|
+
conversations, not official policy. A promise an agent made ("we'll get back
|
|
63
|
+
to you in 48h") is observed practice, not a guarantee, and the field exists so
|
|
64
|
+
nothing downstream states it to a customer as a commitment. `provenance:
|
|
65
|
+
policy` is reserved for a concept a human authored or promoted by hand — the
|
|
66
|
+
engine never sets it.
|
|
67
|
+
|
|
68
|
+
## Enabling it — the `knowledge:` block
|
|
69
|
+
|
|
70
|
+
Knowledge is pack data on the agent, exactly like `distill:` or `harvest:` —
|
|
71
|
+
absent = the feature is off for that agent, byte-identical engine:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
agent = Insika.agent("store-support") do
|
|
75
|
+
instructions "…"
|
|
76
|
+
knowledge extract: true, retrieve: true,
|
|
77
|
+
types: %w[fact policy objection], # what the extractor may emit
|
|
78
|
+
top_k: 5 # concepts injected per turn (default 5)
|
|
79
|
+
# prompt: "<what counts as a concept for THIS store>" — the pack-authored
|
|
80
|
+
# half; absent = the engine's generic prompt. `model:` (absent = the
|
|
81
|
+
# platform utility_model) names the extractor AND the consolidator.
|
|
82
|
+
# index: "scan" (default, the only one built) | "fts5" (accepted, falls
|
|
83
|
+
# back to "scan" — the optional SQLite index isn't built yet).
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`extract` and `retrieve` are independent switches: an agent can learn without
|
|
88
|
+
recalling (write-only, inspected by hand), or recall without learning
|
|
89
|
+
(curate every concept by hand in the Studio, `provenance: policy`).
|
|
90
|
+
|
|
91
|
+
## The write path
|
|
92
|
+
|
|
93
|
+
After a turn completes, off the turn's critical path (the customer already has
|
|
94
|
+
the answer), the engine sends the turn's transcript slice to the platform
|
|
95
|
+
`utility_model` and asks for a JSON array of candidate concepts. The answer is
|
|
96
|
+
schema-validated, filtered against the agent's configured `types`, and any key
|
|
97
|
+
the model should not be writing is dropped and counted, never trusted. The
|
|
98
|
+
survivors are redacted for PII, stamped with provenance and a first-sighting
|
|
99
|
+
confidence, and written to the store — one `:knowledge_learned` event per
|
|
100
|
+
concept (name, type, agent — never content).
|
|
101
|
+
|
|
102
|
+
**Durability, honestly.** Extraction is best-effort: a crash between a turn's
|
|
103
|
+
terminal and the write loses that turn's concepts, not the conversation (which
|
|
104
|
+
is durable) and not a previously learned concept. The recovery path is a
|
|
105
|
+
re-scan, not a queue:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
insika knowledge:backfill --agent store-support --since 2026-08-01T00:00:00Z
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
replays the agent's stored sessions through the same extractor a live turn
|
|
112
|
+
uses.
|
|
113
|
+
|
|
114
|
+
## Consolidation — what happens on a repeat sighting
|
|
115
|
+
|
|
116
|
+
Writing a concept name that already exists is never a blind overwrite. The
|
|
117
|
+
engine compares the new sighting against what is on record and picks one of
|
|
118
|
+
three outcomes:
|
|
119
|
+
|
|
120
|
+
- **Same claim, reworded or reconfirmed** — a cheap, deterministic check
|
|
121
|
+
(no model call): occurrences go up, the new session id joins `sources`,
|
|
122
|
+
confidence climbs (`min(0.95, 0.5 + 0.1 × distinct sources)` — more
|
|
123
|
+
independent sightings, more confidence, never certainty). The body itself
|
|
124
|
+
is untouched, so an operator's edit is never silently discarded by a
|
|
125
|
+
repeat sighting.
|
|
126
|
+
- **Related claim** — the two bodies say compatible things that combine into
|
|
127
|
+
one coherent statement. A second model call (the only place this feature
|
|
128
|
+
spends a second call, and only when a name already exists) merges them;
|
|
129
|
+
the result bumps occurrences/sources/confidence the same way a same-claim
|
|
130
|
+
sighting does.
|
|
131
|
+
- **Contradicting claim** — the two bodies say genuinely different things.
|
|
132
|
+
**Never merged, never silently overwritten.** The new claim is appended
|
|
133
|
+
under a `## Contradiction` heading, confidence drops to a flat `0.4`, and
|
|
134
|
+
a `:knowledge_conflict` event fires. A human resolves it in the Studio by
|
|
135
|
+
editing the concept directly — there is no separate "resolve" action,
|
|
136
|
+
because resolving IS editing the markdown to say what's actually true.
|
|
137
|
+
|
|
138
|
+
When no model is configured for consolidation (or its answer is unusable),
|
|
139
|
+
the engine defaults to the conservative outcome — contradicting. A concept
|
|
140
|
+
this feature is unsure about becomes a human's problem, never a guess that
|
|
141
|
+
looks confident and might be wrong.
|
|
142
|
+
|
|
143
|
+
## The Studio page
|
|
144
|
+
|
|
145
|
+
`/studio/knowledge` — single-agent-scoped like Harvest (`?agent=`), not
|
|
146
|
+
shared like Skills, because a concept only ever belongs to one store. A
|
|
147
|
+
drill-down list (name, type, confidence, occurrences, updated_at) with a
|
|
148
|
+
conflict filter (`?status=conflict`) and the same CodeMirror markdown editor
|
|
149
|
+
Skills uses, version history and restore, and delete. Editing the raw
|
|
150
|
+
markdown is also how an operator promotes `provenance: observed` to `policy`
|
|
151
|
+
— there is no separate "promote" button, the field is just another line in
|
|
152
|
+
the file.
|
|
153
|
+
|
|
154
|
+
## Retrieval — what reaches a turn's prompt
|
|
155
|
+
|
|
156
|
+
With `knowledge.retrieve` on, every turn the engine searches the agent's
|
|
157
|
+
concepts for the ones relevant to the customer's message (pure term overlap —
|
|
158
|
+
no embeddings, no network call) and injects the top few as a level-1
|
|
159
|
+
`<knowledge>` block, the same progressive-disclosure shape Skills uses:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
<knowledge>
|
|
163
|
+
<concept name="cep-13-campinas" confidence="0.60" provenance="observed">CEPs 13xxx-13999 ship from the Campinas DC, 1-2 business days.</concept>
|
|
164
|
+
</knowledge>
|
|
165
|
+
|
|
166
|
+
If the customer's question needs more than the summary above, call
|
|
167
|
+
`load_knowledge("name")` FIRST — before any other lookup for that topic.
|
|
168
|
+
This is learned from past conversations, not official policy: never state
|
|
169
|
+
a `provenance="observed"` concept to the customer as a guarantee.
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Only `name`/`description`/`confidence`/`provenance` are shown — the same
|
|
173
|
+
"summary now, body on demand" shape a skill's level-1 list uses. A `load_knowledge`
|
|
174
|
+
tool (outside `tools_allow`, wired only when `retrieve` is on — same as
|
|
175
|
+
`load_skill`) fetches one concept's complete body. Calling it fires
|
|
176
|
+
`:knowledge_retrieved` — this, not the injection itself, is what the
|
|
177
|
+
adoption metric tracks (see [the honest limits](#the-honest-limits)): a
|
|
178
|
+
concept sitting unread in the prompt taught the agent nothing.
|
|
179
|
+
|
|
180
|
+
**One hop through `[[links]]`.** A matched concept's body may reference
|
|
181
|
+
`[[other-concept-name]]`; retrieval resolves those names against the store
|
|
182
|
+
and injects them too (capped at `top_k` again), so a concept that names its
|
|
183
|
+
neighbor arrives with it. No transitive walk — one level, deliberately, the
|
|
184
|
+
same reasoning a skill's declared `companions:` uses.
|
|
185
|
+
|
|
186
|
+
**Where it sits, and what gets cut first.** `<knowledge>` sits at priority 77
|
|
187
|
+
— below curated skills (80), above a single conversation's memory (75): a
|
|
188
|
+
human's playbook always outranks what the engine inferred, and what the
|
|
189
|
+
engine inferred outranks one customer's chat facts. Never pinned; under
|
|
190
|
+
budget pressure it goes before skills but survives longer than memory,
|
|
191
|
+
briefing, history and request context — see [Context](CONTEXT.md).
|
|
192
|
+
|
|
193
|
+
## External knowledge over MCP
|
|
194
|
+
|
|
195
|
+
A native knowledge base is not the only shape this can take. Mounting a
|
|
196
|
+
third-party knowledge or memory server as MCP tools on an agent is a
|
|
197
|
+
supported pattern, complementary to the native loop above — a deployment can
|
|
198
|
+
run both: this loop learns concepts from ITS OWN traffic automatically, while
|
|
199
|
+
an MCP-mounted server can serve as a synthesis/consultant tool an agent calls
|
|
200
|
+
explicitly.
|
|
201
|
+
|
|
202
|
+
The one adoption lesson worth carrying over regardless of which shape is used:
|
|
203
|
+
a model does not reliably call a recall/lookup tool just because it exists,
|
|
204
|
+
even when it clearly should. A polite "use this when relevant" instruction
|
|
205
|
+
measured close to zero calls under pressure; what worked was an explicit,
|
|
206
|
+
ordered rule naming the competing tools directly — "call the recall tool
|
|
207
|
+
FIRST, before any data lookup, on topics X" — not a sentiment about when it's
|
|
208
|
+
a good idea. Tool *adoption* is a prompt-ordering problem, not a tool-quality
|
|
209
|
+
one, and it is worth measuring (calls per conversation), not assuming.
|
|
210
|
+
|
|
211
|
+
## The honest limits
|
|
212
|
+
|
|
213
|
+
- **Best-effort extraction, re-scan recovery.** Same discipline as Facts: no
|
|
214
|
+
queue, no exactly-once claim. A concept is re-derivable from the session
|
|
215
|
+
transcript, so a missed extraction is recoverable, never lost.
|
|
216
|
+
- **Consolidation trusts the same model that extracts.** The "related vs.
|
|
217
|
+
contradicting" call is a model judgment, not a proof — the conservative
|
|
218
|
+
default (contradicting, when unsure) bounds the failure mode to "a human
|
|
219
|
+
looks at it," never "two different claims silently became one wrong one."
|
|
220
|
+
- **Precision is a forge audit.** The engine guarantees the gates (schema,
|
|
221
|
+
key-stripping, PII redaction, type allowlist); it cannot guarantee the
|
|
222
|
+
model's judgment about what is worth remembering. That is tuned per store,
|
|
223
|
+
the same way a Harvest or Facts prompt is.
|
|
224
|
+
- **Retrieval quality is not adoption.** Injecting the right concept proves
|
|
225
|
+
nothing if the model never reads it — measure `:knowledge_retrieved`
|
|
226
|
+
(retrieval calls per conversation), not just whether the block appeared.
|
|
227
|
+
The explicit, ordered instruction in the block exists because a softer
|
|
228
|
+
"when to use" wording measured close to zero calls in practice.
|
|
229
|
+
- **Term overlap, not understanding.** `Index::Scan` matches words, not
|
|
230
|
+
meaning — a concept phrased very differently from the customer's words
|
|
231
|
+
will not surface even if it answers the question. No embeddings by
|
|
232
|
+
design: revisit only with evidence that retrieval, not
|
|
233
|
+
extraction, is the bottleneck.
|
|
234
|
+
|
|
235
|
+
## Index::Scan's performance, measured not assumed
|
|
236
|
+
|
|
237
|
+
`Index::Scan` keeps a read cache per instance (the context provider holds
|
|
238
|
+
one for the whole process, never rebuilt per turn): a concept's YAML
|
|
239
|
+
frontmatter is parsed once and reused until that concept's own record
|
|
240
|
+
`updated_at` changes, so a write invalidates itself for free. Reproduce
|
|
241
|
+
these numbers with:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
bundle exec ruby scripts/bench_knowledge_index.rb
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
| concepts/agent | p50 | p95 |
|
|
248
|
+
|---|---|---|
|
|
249
|
+
| 50 | 0.28 ms | 0.47 ms |
|
|
250
|
+
| 200 | 1.17 ms | 1.56 ms |
|
|
251
|
+
| 1000 | 6.4 ms | 8.0 ms |
|
|
252
|
+
| 5000 | 35.6 ms | 41.3 ms |
|
|
253
|
+
|
|
254
|
+
At the scale this feature targets for the first year — hundreds of concepts
|
|
255
|
+
per agent — a warm-cache search costs a bit over a millisecond, close to
|
|
256
|
+
the engine's own documented per-turn overhead (see [Benchmark](BENCHMARK.md)).
|
|
257
|
+
Past roughly a thousand concepts it becomes a real, measurable cost again —
|
|
258
|
+
that specific, numeric point is the trigger for building `Index::FTS5`, not
|
|
259
|
+
a guess made in advance.
|
|
260
|
+
|
|
261
|
+
## Export
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
insika knowledge:export --agent store-support --out ./export [--tenant loja-a]
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Writes one `<name>.md` per concept — the storage format IS the export
|
|
268
|
+
format, so this is a dump, not a converter (same discipline as Facts/
|
|
269
|
+
Harvest's own append-only records): each file is the concept's markdown,
|
|
270
|
+
byte for byte, directly consumable by okf-gem (`OKF::Bundle`) or graphify.
|
|
271
|
+
Re-running it is safe — nothing here is lossy, so there is no `--force` to
|
|
272
|
+
reason about.
|
|
273
|
+
|
|
274
|
+
`--format graphml` writes one combined `knowledge.graphml` instead: a node
|
|
275
|
+
per concept (`name`/`type`/`description`/`confidence`/`provenance` as node
|
|
276
|
+
data) and an edge per `[[link]]` that resolves to another concept in the
|
|
277
|
+
same export — a link to a concept outside the scope is dropped, never a
|
|
278
|
+
dangling edge. Hand-built, hand-escaped XML (no new dependency), directly
|
|
279
|
+
openable in Gephi, yEd, or graphify.
|
|
280
|
+
|
|
281
|
+
## What's not here yet
|
|
282
|
+
|
|
283
|
+
- **The optional FTS5 index** — `knowledge.index: "fts5"` is accepted but
|
|
284
|
+
falls back to `Index::Scan`. Deliberately not built yet: `Scan` was
|
|
285
|
+
measured (above), not assumed, and it comfortably meets this feature's
|
|
286
|
+
target scale. A deployment whose concept count is heading past ~1000 per
|
|
287
|
+
agent is the evidence that would justify building the SQLite
|
|
288
|
+
`MATCH`/`bm25()` adapter — not before.
|
|
289
|
+
- **Decay** — recency is a ranking tiebreak today; a real confidence decay
|
|
290
|
+
curve is a later, evidence-driven addition, not a default.
|
data/docs/LOADTEST.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Load test
|
|
3
|
-
parent: Operate
|
|
4
|
-
nav_order:
|
|
3
|
+
parent: Operate
|
|
4
|
+
nav_order: 4
|
|
5
5
|
permalink: /loadtest/
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -67,7 +67,7 @@ frame that carries it.
|
|
|
67
67
|
|
|
68
68
|
```bash
|
|
69
69
|
INSIKA_URL=http://localhost:9292 \
|
|
70
|
-
|
|
70
|
+
INSIKA_GATEWAY_TOKEN=xxx \
|
|
71
71
|
bundle exec ruby scripts/loadtest.rb \
|
|
72
72
|
--agents demo,my-store --concurrency 16 --iterations 3 \
|
|
73
73
|
--message "hi, how are you?"
|
|
@@ -80,7 +80,7 @@ Runs against a local server **or** a remote one (e.g. Railway) — just point
|
|
|
80
80
|
|
|
81
81
|
| Flag | Default | Meaning |
|
|
82
82
|
|------|---------|---------|
|
|
83
|
-
| `--agents a,b,c` | `demo` | comma-separated agent ids (mapped to `model:
|
|
83
|
+
| `--agents a,b,c` | `demo` | comma-separated agent ids (mapped to `model: insika:<agent>`) |
|
|
84
84
|
| `--concurrency N` | `8` | concurrent turns per wave |
|
|
85
85
|
| `--iterations N` | `1` | number of waves per agent |
|
|
86
86
|
| `--message TEXT` | greeting | user message sent every turn |
|
|
@@ -95,14 +95,14 @@ Runs against a local server **or** a remote one (e.g. Railway) — just point
|
|
|
95
95
|
| Env | Default | Meaning |
|
|
96
96
|
|-----|---------|---------|
|
|
97
97
|
| `INSIKA_URL` | `http://localhost:9292` | base URL of the engine |
|
|
98
|
-
| `
|
|
98
|
+
| `INSIKA_GATEWAY_TOKEN` | falls back to `ADMIN_TOKEN`, then `local-demo` | Bearer for `/v1/responses` |
|
|
99
99
|
| `DEEPSEEK_API_KEY` | — | must be configured **on the server** for real turns (not read by the client) |
|
|
100
100
|
|
|
101
101
|
Use `--dry-run` to sanity-check your flags/URL/token before firing real traffic
|
|
102
102
|
(and to confirm the request body without needing a running server):
|
|
103
103
|
|
|
104
104
|
```bash
|
|
105
|
-
INSIKA_URL=http://localhost:9292
|
|
105
|
+
INSIKA_URL=http://localhost:9292 INSIKA_GATEWAY_TOKEN=xxx \
|
|
106
106
|
bundle exec ruby scripts/loadtest.rb --agents demo --concurrency 16 --dry-run
|
|
107
107
|
```
|
|
108
108
|
|
|
@@ -133,7 +133,7 @@ DEEPSEEK_API_KEY=sk-... ./scripts/loadtest-local.sh [WORKERS] [CONCURRENCY]
|
|
|
133
133
|
| Env | Default | Meaning |
|
|
134
134
|
|-----|---------|---------|
|
|
135
135
|
| `DEEPSEEK_API_KEY` | — (required) | real turns hit the provider; also auto-sourced from `.env.local` |
|
|
136
|
-
| `
|
|
136
|
+
| `INSIKA_GATEWAY_TOKEN` | falls back to `ADMIN_TOKEN`, then `local-demo` | Bearer for the sweep |
|
|
137
137
|
| `PORT` | `9299` | bind port for the local Falcon |
|
|
138
138
|
| `AGENT` | `demo` | agent id to load |
|
|
139
139
|
|
|
@@ -158,30 +158,18 @@ the engine, once at the gateway (its `/v1/responses` speaks the same protocol).
|
|
|
158
158
|
Keep `--agents`, `--concurrency`, `--iterations` and `--message` identical, and use
|
|
159
159
|
matching agents on both sides. Compare the printed TTFB/total/cache/error lines.
|
|
160
160
|
|
|
161
|
-
### 4b.
|
|
161
|
+
### 4b. Comparing against an OpenClaw gateway
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
The engine speaks its own wire names (`model: insika:<agent>`, header
|
|
164
|
+
`X-Insika-Agent`), so OpenClaw's `loadtest-gateway.mjs` cannot be pointed at it
|
|
165
|
+
unmodified. For a shadow comparison, run `loadtest.rb` (section 4a) against each
|
|
166
|
+
side with identical `--agents`, `--concurrency`, `--iterations` and `--message`,
|
|
167
|
+
and diff the reports. What you still need in hand:
|
|
166
168
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
node scripts/loadtest-gateway.mjs --agents demo --concurrency 16 --iterations 3
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
Then run the exact same command with `OPENCLAW_GATEWAY_URL` pointing at the real
|
|
175
|
-
gateway, and diff the two reports. This is the shadow comparison the pilot needs.
|
|
176
|
-
|
|
177
|
-
**What the operator must have in hand** (this repo does not vendor OpenClaw):
|
|
178
|
-
|
|
179
|
-
- The OpenClaw checkout containing `scripts/loadtest-gateway.mjs` and Node installed.
|
|
180
|
-
- A **bearer token accepted by both** sides. For the engine that is
|
|
181
|
-
`OPENCLAW_GATEWAY_TOKEN` (see DEPLOY.md); point the gateway run at its own token.
|
|
182
|
-
- **The same agent id provisioned on both** sides (e.g. `demo`) so `model:
|
|
183
|
-
openclaw:<agent>` resolves on each. On the engine, provision via
|
|
184
|
-
`scripts/import_pack.rb`.
|
|
169
|
+
- A bearer token accepted by each side — `INSIKA_GATEWAY_TOKEN` for the engine
|
|
170
|
+
(see DEPLOY.md), the gateway's own token for the gateway run.
|
|
171
|
+
- **The same agent id provisioned on both** sides (e.g. `demo`). On the engine,
|
|
172
|
+
provision via `scripts/import_pack.rb`.
|
|
185
173
|
- The **same provider** (or an equivalent-latency one) behind each, otherwise you
|
|
186
174
|
are comparing providers, not engines.
|
|
187
175
|
- Both endpoints reachable from where you run the client, warmed up (hit `/up` on
|
data/docs/MEDIA.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Media
|
|
3
|
+
parent: Integrate
|
|
4
|
+
nav_order: 3
|
|
5
|
+
permalink: /media/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Media
|
|
9
|
+
|
|
10
|
+
The engine transports media, it never means it. Photos, voice notes and documents
|
|
11
|
+
travel through the message contract as additive content parts; what a picture
|
|
12
|
+
*means* — a fitting room, a product mockup — stays a skill on top.
|
|
13
|
+
|
|
14
|
+
## In — the message contract
|
|
15
|
+
|
|
16
|
+
The engine transports media, it never means it. The message accepts additive
|
|
17
|
+
**content parts** alongside the text — voice notes and photos travel, and any
|
|
18
|
+
skill (a fitting room, an image QA) stays a consumer layer on top:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
curl -X POST /v1/messages?stream=false -H "Authorization: Bearer $TOKEN" \
|
|
22
|
+
-d '{ "agent": "store-support", "session_id": "chat-7",
|
|
23
|
+
"message": "", "parts": [
|
|
24
|
+
{ "type": "audio", "url": "https://cdn.example.com/voz.ogg" },
|
|
25
|
+
{ "type": "image", "url": "https://cdn.example.com/sofa.jpg" },
|
|
26
|
+
{ "type": "document", "url": "https://cdn.example.com/receita.pdf" }
|
|
27
|
+
] }'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- **Audio** is transcribed (RubyLLM STT; model via `INSIKA_STT_MODEL`) and the
|
|
31
|
+
text enters the turn marked `source: "voice"` on the terminal event — the
|
|
32
|
+
consumer's signal the person spoke. A consumer that transcribes itself can
|
|
33
|
+
send the text with `"source": "voice"` directly. A domain vocabulary hint
|
|
34
|
+
(product names, brand terms) rides the transcription as `prompt:` —
|
|
35
|
+
per-agent `stt_prompt` (the DSL setter, or the Studio config form) beats the
|
|
36
|
+
deployment-wide `INSIKA_STT_PROMPT` env, which beats nothing. OPERATOR
|
|
37
|
+
config, never customer input.
|
|
38
|
+
- **Images** attach to the model's ask (vision); the provider bills them and
|
|
39
|
+
the usage flows like any ask. The first image URL is also
|
|
40
|
+
`{{ctx.image_url}}` for data tools — photo analysis outside the prompt, the
|
|
41
|
+
tool's own egress applying when it fetches.
|
|
42
|
+
- **Documents** (a prescription, a recipe, an invoice — most often a PDF)
|
|
43
|
+
attach the same way images do; the first document URL is
|
|
44
|
+
`{{ctx.document_url}}`. A model without document support fails the ask at
|
|
45
|
+
`:ruby_llm` with the provider's own error — the transport does not preflight
|
|
46
|
+
capability. Capped at 10 MB (`MAX_DOCUMENT_BYTES` — "a prescription, not an
|
|
47
|
+
archive"), separately from the 5 MB image cap.
|
|
48
|
+
- Media URLs (audio, image AND document) are fetched by the engine through
|
|
49
|
+
the same egress guard (a private/metadata target is refused — SSRF) and a
|
|
50
|
+
size ceiling per kind (1 MB audio, 5 MB image, 10 MB document: the bytes
|
|
51
|
+
land in this process). A refused, oversized or unreadable part fails the turn
|
|
52
|
+
loudly at the `:media` stage, never a silent drop.
|
|
53
|
+
- **Media alone is a turn.** A voice note with no caption is `parts` and an
|
|
54
|
+
empty `message` — the transcription becomes the message at the `:media`
|
|
55
|
+
stage. A media message never joins another turn (`collect`/`steer` move text
|
|
56
|
+
only, and the parts would be left behind), and a transcription that comes
|
|
57
|
+
back empty fails the turn instead of asking the model about nothing.
|
|
58
|
+
- **Parts are contract at the edge** — a malformed part (unknown type, an
|
|
59
|
+
image/audio/document without `url`, a text without `text`) is a 422 before
|
|
60
|
+
dispatch on `/v1/messages` and `/v1/responses`. `document` is an ADDITIVE
|
|
61
|
+
part type (the compatibility rule in [the /v1 API](API.md) — no
|
|
62
|
+
`Insika-Version` bump needed).
|
|
63
|
+
- `/v1/responses` accepts the OpenAI multimodal shape: `input` as an array of
|
|
64
|
+
text/image/audio/document parts.
|
|
65
|
+
|
|
66
|
+
### Image editing
|
|
67
|
+
|
|
68
|
+
`generate_image` (below) doesn't only generate — it can EDIT an existing
|
|
69
|
+
image, using `RubyLLM.paint`'s `with:`/`mask:`. The tool exposes
|
|
70
|
+
`source_image_urls` (an array — up to 4) and `mask_url`; when the model omits
|
|
71
|
+
`source_image_urls` AND the turn carries an inbound photo, that photo is
|
|
72
|
+
edited by default — no URL round-trip needed for "edit the photo the customer
|
|
73
|
+
just sent". Explicit URLs always win over the default. A text-to-image call
|
|
74
|
+
(no sources at all) is byte-identical to before this feature existed.
|
|
75
|
+
|
|
76
|
+
What the edit MEANS — a virtual try-on, a product mockup on the customer's
|
|
77
|
+
wall — is the calling skill's business; the tool only transports the bytes.
|
|
78
|
+
Not every image model can edit (`dall-e-3` cannot); a call against a
|
|
79
|
+
non-editing model fails at the provider, surfaced verbatim.
|
|
80
|
+
|
|
81
|
+
## Out — generated media
|
|
82
|
+
|
|
83
|
+
The turn can **produce** an image or a voice clip — but only when both sides of
|
|
84
|
+
the gate agree, because nothing leaks by default. The agent declares it may
|
|
85
|
+
generate media (`outputs` on the profile), and the **channel** declares it can
|
|
86
|
+
receive it (`channel.capabilities` on the request):
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
curl -X POST /v1/responses -H "Authorization: Bearer $TOKEN" -d '{
|
|
90
|
+
"model": "insika:store-support", "user": "chat-7",
|
|
91
|
+
"input": "manda a foto do sofá da promoção",
|
|
92
|
+
"channel": { "capabilities": ["image_output", "audio_output"] }
|
|
93
|
+
}'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
agent = Insika.agent("store-support") do
|
|
98
|
+
instructions "…"
|
|
99
|
+
outputs image: { model: "gpt-image-1", size: "1024x1024" }, # the AGENT's half
|
|
100
|
+
tts: { model: "tts-1", voice: "alloy" }
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- **Both gates** must pass for the model to even see the `generate_image` /
|
|
105
|
+
`tts` tools: the agent opted in (`outputs`) and the request declared the
|
|
106
|
+
matching capability (`image_output` / `audio_output` — an unknown value is a
|
|
107
|
+
422, never a silent ignore). The "abstraction admits only what leaks" rule.
|
|
108
|
+
- **The media rides the envelope, never the answer text.** The terminal event
|
|
109
|
+
and the `/v1/responses` completed frame carry an additive `output_parts`
|
|
110
|
+
array — `{ type: "image", mime_type:, base64:, model: }` /
|
|
111
|
+
`{ type: "audio", mime_type:, base64:, model: }`. The model's prose stays
|
|
112
|
+
the `:content` answer; the channel consumes the bytes next to it.
|
|
113
|
+
- **Generation is billed and counted.** Image tokens join the turn's usage
|
|
114
|
+
(like any ask). The speech API reports no token counts, so a TTS call adds
|
|
115
|
+
an honest `usage.media` counter and the part carries the `model` for
|
|
116
|
+
consumer-side pricing.
|
|
117
|
+
- **Seams, not magic.** The generator is injectable per kind (specs stub it);
|
|
118
|
+
the defaults are lazy: images via RubyLLM (paint), speech via a thin POST to
|
|
119
|
+
the OpenAI-compatible `/audio/speech` endpoint using the same provider
|
|
120
|
+
config the chat uses — RubyLLM as of 1.16.0 has no speech API. A generated
|
|
121
|
+
part over 8 MB refuses loudly, never silently truncates.
|
|
122
|
+
- **Not here:** what the generated image *means* — a fitting room, a product
|
|
123
|
+
mockup — is a skill on top. The engine transports bytes and cost.
|
|
124
|
+
|
|
125
|
+
## See also
|
|
126
|
+
|
|
127
|
+
- [The /v1 API](API.md) — the full message contract.
|
|
128
|
+
- [Channels](CHANNELS.md) — how WhatsApp and the web widget carry these parts.
|
data/docs/OBSERVABILITY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Observability
|
|
3
|
-
parent: Operate
|
|
3
|
+
parent: Operate
|
|
4
4
|
nav_order: 1
|
|
5
5
|
permalink: /observability/
|
|
6
6
|
---
|
|
@@ -25,7 +25,9 @@ authoring writes (`:golden_written`, `:agent_file_written`, …), queue bookkeep
|
|
|
25
25
|
channel delivery (`:channel_delivered` — see [Channels](CHANNELS.md))
|
|
26
26
|
travel the same stream and are **ignored** by the bridge: they open no span and
|
|
27
27
|
touch no instrument, because they are not part of a turn's latency or cost. Any
|
|
28
|
-
other subscriber still sees them.
|
|
28
|
+
other subscriber still sees them. The one mid-turn event the bridge does consume
|
|
29
|
+
is `:tool_loop_intervened` — it feeds the `insika.tool.loop_intervened` counter
|
|
30
|
+
(no span: the intervention is a fact about the turn, not a timed operation).
|
|
29
31
|
|
|
30
32
|
They are worth subscribing to even so, because each is the ONLY record of
|
|
31
33
|
something that left no task of its own behind:
|
|
@@ -36,23 +38,28 @@ something that left no task of its own behind:
|
|
|
36
38
|
| `:turn_steered` | `task_id`, `count`, `total` | a message arrived mid-run and was appended to the turn in flight |
|
|
37
39
|
| `:turn_steer_released` | `task_id`, `released_as`, `count` | the run could not absorb it, so it became the turn `released_as` |
|
|
38
40
|
| `:turn_interrupted` | `task_id`, `replaced_by` | the turn was abandoned mid-run, and which turn replaced it |
|
|
39
|
-
| `:turn_stuck` | `task_id`, `agent`, `reason`, `message` | the agent declared it could not proceed (`signal_stuck
|
|
41
|
+
| `:turn_stuck` | `task_id`, `agent`, `reason`, `message` | the agent declared it could not proceed (`signal_stuck`) — the deterministic signal a consumer escalates on |
|
|
40
42
|
| `:channel_delivered` | `channel`, `outbox_id`, `status`, `attempts`, `error` | the answer reached the platform (or did not) — the turn completing says nothing about that |
|
|
41
|
-
| `:delivery_failed` | `channel`, `outbox_id`, `status`, `attempts`, `error` | a delivery exhausted its bounded retries — the alert face of the row above
|
|
42
|
-
| `:budget_warning` | `agent`, `tenant`, `window`, `spent`, `cap` | a calendar budget crossed its threshold (`alert_at` or a soft cap) — once per window
|
|
43
|
-
| `:breaker_open` | `agent`, `ref`, `tenant` | the reliability circuit breaker tripped for a `(tenant, provider/model)` — further turns fail fast until the cooldown
|
|
44
|
-
| `:provider_failure` | `agent`, `ref`, `error`, `kind` | one attempt against `ref` failed and spent a retry — emitted with or without a circuit breaker
|
|
45
|
-
| `:provider_fallback` | `agent`, `from`, `to`, `error`, `kind` | the turn ROTATED mid-flight to the next node of the fallback chain, and the error that caused it
|
|
46
|
-
| `:ttft` | `task_id`, `session_id`, `ttft_ms` | the provider's time-to-first-token on the streaming envelope — only under `INSIKA_TURN_TIMING`, once per turn
|
|
47
|
-
| `:distillation_completed` | `session_ref`, `agent`, `proposals`, `dropped{}`, `deduped`, `cost{}` | a session's traffic was distilled into N proposals
|
|
43
|
+
| `:delivery_failed` | `channel`, `outbox_id`, `status`, `attempts`, `error` | a delivery exhausted its bounded retries — the alert face of the row above |
|
|
44
|
+
| `:budget_warning` | `agent`, `tenant`, `window`, `spent`, `cap` | a calendar budget crossed its threshold (`alert_at` or a soft cap) — once per window |
|
|
45
|
+
| `:breaker_open` | `agent`, `ref`, `tenant` | the reliability circuit breaker tripped for a `(tenant, provider/model)` — further turns fail fast until the cooldown |
|
|
46
|
+
| `:provider_failure` | `agent`, `ref`, `error`, `kind` | one attempt against `ref` failed and spent a retry — emitted with or without a circuit breaker |
|
|
47
|
+
| `:provider_fallback` | `agent`, `from`, `to`, `error`, `kind` | the turn ROTATED mid-flight to the next node of the fallback chain, and the error that caused it |
|
|
48
|
+
| `:ttft` | `task_id`, `session_id`, `ttft_ms` | the provider's time-to-first-token on the streaming envelope — only under `INSIKA_TURN_TIMING`, once per turn |
|
|
49
|
+
| `:distillation_completed` | `session_ref`, `agent`, `proposals`, `dropped{}`, `deduped`, `cost{}` | a session's traffic was distilled into N proposals — counts and ids only, never a fact value |
|
|
48
50
|
| `:proposal_approved` | `proposal_id`, `status`, `operator` | a human approved a distilled fact on the Facts page — written to memory via CAS |
|
|
49
51
|
| `:proposal_rejected` | `proposal_id`, `status`, `operator` | a human rejected it (the reason is on the proposal record, not in the event) |
|
|
50
52
|
| `:proposal_dismissed` | `proposal_id`, `status`, `operator` | a human dismissed it — the tuple is latched, never proposed again |
|
|
51
53
|
| `:proposal_stale` | `proposal_id`, `status`, `operator` | the CAS lost: the fact moved after distillation — re-presented with both values on the wiki, never silently overwritten |
|
|
52
|
-
| `:harvest_mined` | `agent`, `run_id`, `candidates`, `rejected{}`, `cost{}` | a mining pass finished
|
|
54
|
+
| `:harvest_mined` | `agent`, `run_id`, `candidates`, `rejected{}`, `cost{}` | a mining pass finished — counts and rule ids only, never a skill body |
|
|
53
55
|
| `:harvest_gated` | `run_id`, `candidate_id`, `agent`, `eval_passed`, `conversion_passed`, `reason` | the double gate's verdict on one candidate — ids and verdicts only |
|
|
54
56
|
| `:skill_promoted` | `agent`, `skill`, `candidate_id`, `snapshot_ref`, `promotion_ref`, `approver` | a human approved a mined skill — it is live for the store, with the snapshot for rollback |
|
|
55
57
|
| `:skill_rolled_back` | `snapshot_ref`, `skill`, `agent`, `operator` | the snapshot was restored — the promotion row carries the `rolled_back_at` stamp |
|
|
58
|
+
| `:knowledge_learned` | `name`, `type`, `agent` | a turn's transcript taught the agent a new concept, or a related sighting merged into an existing one — name and type only, never the body |
|
|
59
|
+
| `:knowledge_conflict` | `name`, `agent` | a repeat sighting contradicted what's on record — never merged, appended under a heading, needs a human in the Studio |
|
|
60
|
+
| `:knowledge_deleted` | `name`, `agent` | an operator removed a concept from the Studio |
|
|
61
|
+
| `:knowledge_retrieved` | `name`, `agent` | the model called `load_knowledge` — the adoption signal (retrieval calls per conversation), not the `<knowledge>` injection itself |
|
|
62
|
+
| `:knowledge_backfilled` | `agent`, `sessions`, `concepts`, `conflicts`, `dropped{}` | the recovery re-scan (`insika knowledge:backfill`) finished replaying an agent's stored sessions |
|
|
56
63
|
|
|
57
64
|
`delivery_failed` and `breaker_open` are the two the operator config is pointed at
|
|
58
65
|
(`alerts.webhook` on the profile): each only fires when something durable did
|
|
@@ -140,12 +147,32 @@ knows its outcome.
|
|
|
140
147
|
| `insika.cost` | counter | `{USD}` | the turn's model is priced (see below) |
|
|
141
148
|
| `insika.tool.calls` | counter | `{call}` | a tool call completes |
|
|
142
149
|
| `insika.tool.duration` | histogram | `s` | a `tool_call`/`tool_result` pair completes |
|
|
150
|
+
| `insika.cache.hit_rate` | histogram | `%` | a turn reported billed prompt tokens (see below) |
|
|
151
|
+
| `insika.tool.loop_intervened` | counter | `{intervention}` | the loop detector delivered its one-shot warning |
|
|
152
|
+
| `insika.context.compacted` | counter | `{compaction}` | an in-session compaction was persisted (RFC-0044) |
|
|
143
153
|
|
|
144
154
|
`insika.tool.duration` is deliberately **not** recorded for data-tools: those are a
|
|
145
155
|
single point-in-time event, so there is no measured duration to report. A tool left
|
|
146
156
|
open by a mid-turn failure is not counted as a completed call either — its span is
|
|
147
157
|
closed, but a failed call must not inflate the success histogram.
|
|
148
158
|
|
|
159
|
+
`insika.cache.hit_rate` is the same arithmetic the Studio's per-agent series uses:
|
|
160
|
+
cache **reads** over the whole **billed** prompt (fresh input + cache reads + cache
|
|
161
|
+
writes), always in `[0,100]`. A turn with no billed prompt tokens records nothing —
|
|
162
|
+
absence is not a 0% hit. It is recorded per turn on the terminal event, so it
|
|
163
|
+
carries the turn labels (`insika.agent`, `insika.model`, …); the token-counter
|
|
164
|
+
recipe below still works and answers the fleet-wide version of the question.
|
|
165
|
+
|
|
166
|
+
`insika.tool.loop_intervened` counts deliveries of the loop detector's one
|
|
167
|
+
warning per turn (see [Agents](AGENTS.md) `max_tool_repeat`), labelled with
|
|
168
|
+
`insika.tool`. It counts interventions, not repeats: a turn contributes at most 1.
|
|
169
|
+
|
|
170
|
+
`insika.context.compacted` counts persisted in-session compactions
|
|
171
|
+
(`:context_compacted` — see [Context](CONTEXT.md)), labelled with
|
|
172
|
+
`insika.agent` and `insika.model` (the summarizer's model, not the turn's). It
|
|
173
|
+
fires post-turn, after the turn span already closed, so it deliberately rides
|
|
174
|
+
its own labels rather than the open-turn set.
|
|
175
|
+
|
|
149
176
|
## Attribute reference
|
|
150
177
|
|
|
151
178
|
The same names are used on spans and on metrics. **Metrics carry a deliberate
|
|
@@ -262,7 +289,14 @@ climbing while `input` stays flat.
|
|
|
262
289
|
|
|
263
290
|
**Cache hit ratio**
|
|
264
291
|
`insika.tokens` filtered to `insika.token.type="cached"` over the same counter
|
|
265
|
-
filtered to `input`. This is the number that moves your bill.
|
|
292
|
+
filtered to `input`. This is the number that moves your bill. For the per-turn
|
|
293
|
+
distribution (does every turn hit, or do fleet averages hide cold agents?), chart
|
|
294
|
+
`insika.cache.hit_rate` — p50 by `insika.agent`; a healthy agent sits near 100.
|
|
295
|
+
|
|
296
|
+
**Loop interventions**
|
|
297
|
+
`insika.tool.loop_intervened`, rate, grouped by `insika.agent` and `insika.tool`.
|
|
298
|
+
Any sustained non-zero rate means one tool keeps being retried with identical
|
|
299
|
+
arguments — fix the tool's contract or the prompt, not the detector.
|
|
266
300
|
|
|
267
301
|
**Spend per tenant**
|
|
268
302
|
`insika.cost`, rate (or `increase` over a billing window), grouped by
|