insika 0.8.0 → 1.0.1
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 +312 -2
- data/README.md +10 -6
- data/bin/insika +4 -1
- data/bin/insika-router +0 -1
- data/docs/AGENTS.md +142 -18
- data/docs/API.md +73 -0
- data/docs/ARCHITECTURE.md +56 -50
- data/docs/ARTIFACTS.md +33 -3
- data/docs/BENCHMARK.md +50 -7
- data/docs/CHANNELS.md +19 -2
- data/docs/CONTEXT.md +96 -27
- data/docs/CROSS-HARNESS-BENCH.md +157 -0
- data/docs/DEPLOY.md +70 -13
- data/docs/EMBEDDING.md +10 -0
- data/docs/EVALS.md +129 -10
- data/docs/FACTS.md +12 -6
- data/docs/HARVEST.md +8 -3
- data/docs/KNOWLEDGE.md +14 -0
- data/docs/MEDIA.md +6 -2
- data/docs/OBSERVABILITY.md +164 -28
- data/docs/PERFORMANCE.md +110 -0
- data/docs/PLUGINS.md +4 -4
- data/docs/POLICY.md +26 -14
- data/docs/RELEASING.md +9 -0
- data/docs/ROUTER.md +7 -0
- data/docs/RUBYLLM_2_MIGRATION.md +307 -0
- data/docs/SECURITY.md +34 -1
- data/docs/SHARED-CONVERSATIONS.md +40 -0
- data/docs/SKILLS.md +5 -3
- data/docs/STUDIO.md +122 -0
- data/docs/TOOLS.md +238 -42
- data/docs/operate.md +3 -0
- data/docs/prompts/ADD-TOOL.md +12 -2
- data/docs/prompts/DIAGNOSE-TURN.md +3 -0
- data/docs/prompts/GO-LIVE.md +3 -1
- data/lib/insika/agent_defaults.rb +140 -0
- data/lib/insika/agent_file_store.rb +12 -0
- data/lib/insika/agent_profile.rb +99 -17
- data/lib/insika/artifact_signing.rb +11 -0
- data/lib/insika/channels/web/widget.js +33 -0
- data/lib/insika/channels/web.rb +5 -2
- data/lib/insika/chat_builder.rb +124 -28
- data/lib/insika/checkpoint.rb +6 -1
- data/lib/insika/checkpoint_store.rb +59 -7
- data/lib/insika/commands/agent_payload.rb +3 -3
- data/lib/insika/commands/delete_kit.rb +26 -0
- data/lib/insika/commands/gate_harvest.rb +3 -0
- data/lib/insika/commands/review_guardrail_flag.rb +38 -0
- data/lib/insika/commands/run_distillation.rb +5 -8
- data/lib/insika/commands/run_harvest.rb +17 -1
- data/lib/insika/commands/seed_session.rb +128 -0
- data/lib/insika/commands/send_message.rb +9 -3
- data/lib/insika/commands/update_settings.rb +23 -1
- data/lib/insika/commands/write_kit.rb +27 -0
- data/lib/insika/compaction.rb +33 -7
- data/lib/insika/config_store.rb +87 -7
- data/lib/insika/context/builder.rb +29 -9
- data/lib/insika/context/priority.rb +5 -0
- data/lib/insika/context/provider.rb +7 -2
- data/lib/insika/context/providers/briefing.rb +11 -8
- data/lib/insika/context/providers/fence_notice.rb +27 -0
- data/lib/insika/context/providers/knowledge.rb +32 -6
- data/lib/insika/context/providers/memory.rb +61 -8
- data/lib/insika/context/providers/pending_confirmation.rb +52 -0
- data/lib/insika/context/providers/prompt.rb +17 -4
- data/lib/insika/context/providers/session.rb +46 -6
- data/lib/insika/context/reranker.rb +75 -0
- data/lib/insika/distill.rb +22 -13
- data/lib/insika/doctor.rb +142 -4
- data/lib/insika/dsl/runtime.rb +37 -4
- data/lib/insika/dsl/server_boot.rb +4 -12
- data/lib/insika/dsl.rb +19 -5
- data/lib/insika/edge_limiter.rb +27 -16
- data/lib/insika/egress_guard.rb +11 -3
- data/lib/insika/env_schema.rb +33 -73
- data/lib/insika/errors.rb +1 -0
- data/lib/insika/evals/assertions.rb +259 -18
- data/lib/insika/evals/golden.rb +230 -4
- data/lib/insika/evals/judge.rb +19 -10
- data/lib/insika/evals/moderator_compare.rb +47 -0
- data/lib/insika/evals/report.rb +61 -9
- data/lib/insika/evals/runner.rb +64 -4
- data/lib/insika/evals/simulator.rb +21 -3
- data/lib/insika/evals/transport.rb +317 -15
- data/lib/insika/event_stream.rb +63 -7
- data/lib/insika/evidence.rb +105 -17
- data/lib/insika/executor.rb +698 -164
- data/lib/insika/fence.rb +96 -0
- data/lib/insika/golden_store.rb +3 -0
- data/lib/insika/harvest.rb +26 -14
- data/lib/insika/harvest_engine.rb +1 -0
- data/lib/insika/harvest_store.rb +12 -0
- data/lib/insika/http_client.rb +87 -4
- data/lib/insika/kits.rb +54 -0
- data/lib/insika/knowledge.rb +34 -20
- data/lib/insika/llm_configurator.rb +46 -4
- data/lib/insika/llm_provider_store.rb +1 -3
- data/lib/insika/llm_trace_store.rb +54 -0
- data/lib/insika/mcp_client.rb +27 -63
- data/lib/insika/mcp_live_tool.rb +55 -9
- data/lib/insika/mcp_store.rb +37 -3
- data/lib/insika/mcp_tool_registry.rb +76 -26
- data/lib/insika/media.rb +38 -25
- data/lib/insika/memory_store.rb +16 -3
- data/lib/insika/memory_trim.rb +60 -0
- data/lib/insika/middleware.rb +12 -3
- data/lib/insika/model_metrics_store.rb +285 -0
- data/lib/insika/model_registry.rb +110 -0
- data/lib/insika/model_resolver.rb +1 -1
- data/lib/insika/model_selection.rb +14 -21
- data/lib/insika/model_visible.rb +17 -6
- data/lib/insika/onboarding.rb +1 -0
- data/lib/insika/outcome_store.rb +1 -1
- data/lib/insika/overlay_tool_registry.rb +30 -2
- data/lib/insika/pack_importer.rb +54 -9
- data/lib/insika/packaging.rb +14 -5
- data/lib/insika/pending_action_store.rb +45 -18
- data/lib/insika/plugin/loader.rb +4 -2
- data/lib/insika/prefix_fingerprint.rb +32 -27
- data/lib/insika/profile_source.rb +5 -0
- data/lib/insika/provider_error_classifier.rb +18 -3
- data/lib/insika/queue_policy.rb +7 -2
- data/lib/insika/refinement/proposer.rb +42 -17
- data/lib/insika/reliability.rb +12 -3
- data/lib/insika/router/app.rb +14 -5
- data/lib/insika/router/session_key.rb +35 -5
- data/lib/insika/safety/config.rb +20 -3
- data/lib/insika/safety/corpus.rb +10 -3
- data/lib/insika/safety/factory.rb +39 -1
- data/lib/insika/safety/input_guardrail.rb +21 -1
- data/lib/insika/safety/judge_moderator.rb +70 -0
- data/lib/insika/safety/output_validator.rb +29 -5
- data/lib/insika/safety/prompt_echo.rb +89 -0
- data/lib/insika/schema_guard.rb +53 -76
- data/lib/insika/server/app.rb +58 -7
- data/lib/insika/server/rack_app.rb +2 -0
- data/lib/insika/server/responses.rb +50 -5
- data/lib/insika/session_actor.rb +58 -28
- data/lib/insika/session_store.rb +163 -11
- data/lib/insika/settings_store.rb +57 -2
- data/lib/insika/shared_conversations.rb +305 -0
- data/lib/insika/skill_catalog.rb +28 -2
- data/lib/insika/skill_store.rb +6 -0
- data/lib/insika/soak/runner.rb +1 -1
- data/lib/insika/spoken_transcript.rb +31 -0
- data/lib/insika/steer_injector.rb +7 -48
- data/lib/insika/store.rb +29 -1
- data/lib/insika/stores/json_value.rb +37 -0
- data/lib/insika/stores/memory.rb +22 -35
- data/lib/insika/stores/postgres.rb +295 -0
- data/lib/insika/stores/sqlite.rb +257 -43
- data/lib/insika/stores/turn_counter.rb +25 -0
- data/lib/insika/studio/app.rb +1459 -163
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +25 -25
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-sans-latin-wght-normal.woff2 +0 -0
- data/lib/insika/studio/forms.rb +189 -12
- data/lib/insika/studio/nav_icons.rb +3 -0
- data/lib/insika/studio/views/_agent_config_retrieval.erb +73 -0
- data/lib/insika/studio/views/_agent_picker.erb +15 -0
- data/lib/insika/studio/views/_agent_tab_config.erb +83 -243
- data/lib/insika/studio/views/_agent_tab_history.erb +56 -1
- data/lib/insika/studio/views/_agent_tab_performance.erb +34 -0
- data/lib/insika/studio/views/_agent_tab_prompts.erb +7 -9
- data/lib/insika/studio/views/_agent_tab_skills.erb +16 -7
- data/lib/insika/studio/views/_agent_tab_tools.erb +90 -0
- data/lib/insika/studio/views/_agents_master.erb +8 -12
- data/lib/insika/studio/views/_cfg_alerts.erb +4 -0
- data/lib/insika/studio/views/_cfg_budget.erb +20 -0
- data/lib/insika/studio/views/_cfg_distill.erb +26 -0
- data/lib/insika/studio/views/_cfg_followup.erb +30 -0
- data/lib/insika/studio/views/_cfg_grounding.erb +21 -0
- data/lib/insika/studio/views/_cfg_guardrails.erb +47 -0
- data/lib/insika/studio/views/_cfg_harvest.erb +35 -0
- data/lib/insika/studio/views/_cfg_reliability.erb +32 -0
- data/lib/insika/studio/views/_chats_master.erb +66 -0
- data/lib/insika/studio/views/_customers_master.erb +37 -0
- data/lib/insika/studio/views/_empty.erb +15 -0
- data/lib/insika/studio/views/_facts_master.erb +42 -0
- data/lib/insika/studio/views/_harvest_candidate_detail.erb +14 -0
- data/lib/insika/studio/views/_identity.erb +16 -0
- data/lib/insika/studio/views/_kpi.erb +17 -0
- data/lib/insika/studio/views/_message.erb +40 -2
- data/lib/insika/studio/views/_model_chart.erb +82 -0
- data/lib/insika/studio/views/_refinement_master.erb +38 -0
- data/lib/insika/studio/views/{tool_edit.erb → _tool_edit.erb} +38 -11
- data/lib/insika/studio/views/_tool_users.erb +17 -0
- data/lib/insika/studio/views/_why.erb +53 -0
- data/lib/insika/studio/views/agent_detail.erb +17 -10
- data/lib/insika/studio/views/agents.erb +28 -16
- data/lib/insika/studio/views/approvals.erb +16 -11
- data/lib/insika/studio/views/artifact.erb +20 -7
- data/lib/insika/studio/views/artifacts.erb +19 -14
- data/lib/insika/studio/views/chats.erb +22 -29
- data/lib/insika/studio/views/customer.erb +88 -43
- data/lib/insika/studio/views/customers.erb +16 -26
- data/lib/insika/studio/views/evals.erb +49 -31
- data/lib/insika/studio/views/fact_detail.erb +114 -0
- data/lib/insika/studio/views/facts.erb +15 -116
- data/lib/insika/studio/views/followups.erb +63 -52
- data/lib/insika/studio/views/funnel.erb +104 -43
- data/lib/insika/studio/views/harvest.erb +85 -59
- data/lib/insika/studio/views/home.erb +138 -23
- data/lib/insika/studio/views/inbox.erb +36 -0
- data/lib/insika/studio/views/kits.erb +176 -0
- data/lib/insika/studio/views/knowledge.erb +92 -80
- data/lib/insika/studio/views/layout.erb +42 -11
- data/lib/insika/studio/views/live.erb +45 -0
- data/lib/insika/studio/views/mcp.erb +43 -13
- data/lib/insika/studio/views/models.erb +187 -0
- data/lib/insika/studio/views/parity.erb +38 -32
- data/lib/insika/studio/views/playground.erb +13 -9
- data/lib/insika/studio/views/refinement.erb +15 -224
- data/lib/insika/studio/views/refinement_detail.erb +243 -0
- data/lib/insika/studio/views/session.erb +153 -118
- data/lib/insika/studio/views/settings.erb +86 -22
- data/lib/insika/studio/views/skills.erb +53 -19
- data/lib/insika/studio/views/system_files.erb +7 -7
- data/lib/insika/studio/views/task.erb +45 -15
- data/lib/insika/studio/views/tasks.erb +35 -18
- data/lib/insika/studio/views/tools.erb +62 -73
- data/lib/insika/task_actor.rb +4 -1
- data/lib/insika/task_store.rb +67 -1
- data/lib/insika/telemetry/pricing.rb +16 -14
- data/lib/insika/telemetry/recorder.rb +115 -9
- data/lib/insika/telemetry/ruby_llm_instrumenter.rb +110 -0
- data/lib/insika/telemetry.rb +12 -2
- data/lib/insika/templates/daily-digest/README.md +13 -0
- data/lib/insika/templates/daily-digest/agent.rb +22 -11
- data/lib/insika/testing/store_contract.rb +65 -0
- data/lib/insika/text_tool_call.rb +66 -0
- data/lib/insika/tick.rb +7 -1
- data/lib/insika/token_store.rb +5 -0
- data/lib/insika/tool_assembly.rb +21 -13
- data/lib/insika/tool_batch.rb +1 -1
- data/lib/insika/tool_catalog.rb +12 -2
- data/lib/insika/tool_definition.rb +82 -13
- data/lib/insika/tool_envelope.rb +266 -29
- data/lib/insika/tool_manifest.rb +1 -0
- data/lib/insika/tool_store.rb +12 -4
- data/lib/insika/tool_trace_store.rb +15 -8
- data/lib/insika/tool_usage_report.rb +14 -4
- data/lib/insika/tools/a2a_remote.rb +1 -1
- data/lib/insika/tools/agent_enum.rb +1 -1
- data/lib/insika/tools/confirm_pending.rb +112 -0
- data/lib/insika/tools/data_defined_tool.rb +24 -15
- data/lib/insika/tools/generate_image.rb +2 -2
- data/lib/insika/tools/load_knowledge.rb +7 -3
- data/lib/insika/tools/load_skill.rb +3 -3
- data/lib/insika/tools/present.rb +115 -0
- data/lib/insika/tools/remember.rb +7 -2
- data/lib/insika/tools/run_persona_eval.rb +9 -6
- data/lib/insika/tools/save_artifact.rb +16 -6
- data/lib/insika/tools/schedule_followup.rb +3 -3
- data/lib/insika/tools/stuck_signal.rb +3 -3
- data/lib/insika/tools/subagent.rb +5 -5
- data/lib/insika/tools/subagents.rb +3 -3
- data/lib/insika/tools/tool_search.rb +11 -7
- data/lib/insika/tools/tts.rb +2 -2
- data/lib/insika/tools/update_briefing.rb +4 -4
- data/lib/insika/turn_output.rb +2 -2
- data/lib/insika/turn_state.rb +31 -1
- data/lib/insika/turn_timing.rb +51 -14
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +88 -16
- data/lib/insika/wiring/graph_chat.rb +4 -0
- data/lib/insika/workflow.rb +25 -64
- data/lib/insika.rb +23 -3
- metadata +74 -13
- data/lib/insika/commands/import_mcp_tools.rb +0 -48
- data/lib/insika/mcp_http_client.rb +0 -67
- data/lib/insika/mcp_tool_ingestor.rb +0 -165
data/docs/EVALS.md
CHANGED
|
@@ -42,6 +42,117 @@ 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 on the LAST turn (`?` = optional, never fails) |
|
|
113
|
+
| `turns[i].tools_called: [names]` | the calls one specific turn must make — a two-turn case where turn one claimed an action and turn two repaired the store passes the key above and fails this one |
|
|
114
|
+
| `never_calls: [names]` | none of these was called — the negative every `tools_called` needs |
|
|
115
|
+
| `calls_one_of: [names]` | at least one of these was called |
|
|
116
|
+
| `first_tool: name` | the first call's name |
|
|
117
|
+
| `max_tool_calls: N` | a ceiling on the turn's calls |
|
|
118
|
+
| `reply_includes: [substrings]` | each appears in the published answer (case-insensitive) |
|
|
119
|
+
| `reply_omits: [substrings]` | none appears — where an internal id, a CPF or a raw tag leaking into the customer's text is pinned |
|
|
120
|
+
| `blocked_gates: ["tool:gate"]` | each pair appears among the turn's blocked calls |
|
|
121
|
+
| `ui_components: [names]` | a presentation tool showed at least one card of each component (the `insika.ui` frames) |
|
|
122
|
+
| `no_ui: true` | the turn showed nothing — the negative of `ui_components` |
|
|
123
|
+
| `must_not: [detectors]` | the negative detectors (`pii_leak`, `tool_error`, `phantom_action`, …); a blocked call is not a tool error |
|
|
124
|
+
| `claims: { tool: pattern }` | what `phantom_action` reads — per mutation, the words a reply uses to say it did it (a regex, case-insensitive); the store's language, so it lives in the case |
|
|
125
|
+
|
|
126
|
+
**Every positive has a negative.** A case that only says `tools_called: [add_to_cart]`
|
|
127
|
+
passes an agent that also re-searched, or that echoed the SKU to the customer. Pin
|
|
128
|
+
what a correct turn does *not* do — `never_calls`, `reply_omits`, `max_tool_calls` —
|
|
129
|
+
in the same case. Snapshots avoid the model calls needed to reconstruct the setup;
|
|
130
|
+
use the simulator when later customer messages need to branch on the replies.
|
|
131
|
+
|
|
132
|
+
`tools_called` confirms an attempted call, not a successful backend mutation.
|
|
133
|
+
|
|
134
|
+
`must_not: phantom_action` reads the other direction: a reply that SAYS it did
|
|
135
|
+
something ("adicionei", "removi", "fechei o pedido") on a turn where the matching
|
|
136
|
+
tool never ran, errored, or was blocked. It is cumulative over the conversation, so
|
|
137
|
+
"já adicionei" on turn two is true when turn one did it, and it only pins the words
|
|
138
|
+
against the calls — a cart that ended up right by accident is still a lie to the
|
|
139
|
+
customer. The words come from the case's `claims:` map, in the store's own
|
|
140
|
+
language, past tense on purpose: "quer adicionar?" is a question.
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
expect:
|
|
144
|
+
must_not: [tool_error, phantom_action]
|
|
145
|
+
claims:
|
|
146
|
+
add_to_cart: '\b(?:adicion(?:ei|ad[oa]s?)|coloquei)\b'
|
|
147
|
+
create_order: '\b(?:fechei|finalizei|pedido (?:criado|fechado))\b'
|
|
148
|
+
```
|
|
149
|
+
Arguments are collected for inspection; there is no generic argument or cart-state
|
|
150
|
+
grader. `blocked_gates` needs completion statuses from the HTTP transport. The
|
|
151
|
+
in-process `GraphTransport` records tool names and UI events, but not completion
|
|
152
|
+
statuses or arguments. Neither transport counts `load_skill` / `load_knowledge` as
|
|
153
|
+
tool calls. `ui_components` requires a nonempty selection; `no_ui`
|
|
154
|
+
accepts absent UI events and events with zero items.
|
|
155
|
+
|
|
45
156
|
### `requires` — a case that cannot run here is skipped, not failed
|
|
46
157
|
|
|
47
158
|
Deployments differ. Some stores have order tracking wired, some do not; some run
|
|
@@ -90,12 +201,13 @@ judge is told the rule instead of guessing it.
|
|
|
90
201
|
|
|
91
202
|
```yaml
|
|
92
203
|
expect:
|
|
93
|
-
policy: ask_once
|
|
204
|
+
policy: ask_once # the strict reading: one question, full stop
|
|
205
|
+
policy: { ask_once: { max: 2 } } # a store that greets its customers
|
|
94
206
|
```
|
|
95
207
|
|
|
96
208
|
| policy | the check | the judge is told |
|
|
97
209
|
|---|---|---|
|
|
98
|
-
| `ask_once` | no reply asks more than
|
|
210
|
+
| `ask_once` | no reply asks more than `max` questions (default 1) | at most `max` per reply, and a greeting formula is courtesy rather than a question |
|
|
99
211
|
| `investigate_first` | turn 1 asks something and calls no tool | ask on a vague request, don't search immediately |
|
|
100
212
|
| `act_fast` | turn 1 calls a tool | asking what a search would answer is a failure |
|
|
101
213
|
|
|
@@ -108,6 +220,16 @@ so a tracking link's query string is not read as the agent asking something. It
|
|
|
108
220
|
policy signal, not grammar — and crude was enough to catch an agent breaking a rule
|
|
109
221
|
written in its own prompt, twice, with no model in the loop.
|
|
110
222
|
|
|
223
|
+
**`max` is the store's, and it matters more than it looks.** The default of 1 comes from
|
|
224
|
+
a real store's rule, quoted down to the punctuation: *"Máximo 1 pergunta principal por
|
|
225
|
+
mensagem = 1 ponto de interrogação no fim"*. A store that greets its customers spends
|
|
226
|
+
one of those on courtesy — measured across six harnesses answering the same "oi", `max:
|
|
227
|
+
1` failed every reply containing "tudo bem?" and passed every reply without it, and
|
|
228
|
+
nothing else. That grades a greeting habit, not the rule the policy exists for, which is
|
|
229
|
+
the form: *"qual seu nome? qual o número do pedido? e o motivo?"*. Set `max` to what
|
|
230
|
+
your store actually tolerates; the judge is told the same number, so the two halves
|
|
231
|
+
never grade different rules.
|
|
232
|
+
|
|
111
233
|
### `reference` — compared against the system you want to replace
|
|
112
234
|
|
|
113
235
|
`min_score: 0.7` says a reply cleared a bar you invented. It says nothing about
|
|
@@ -269,14 +391,11 @@ The model only sees `case_id`, enumerated with the ids the tool can actually run
|
|
|
269
391
|
(never a free string it could invent) — every **simulated** case in the store, the same
|
|
270
392
|
`persona:` shape as above.
|
|
271
393
|
|
|
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.
|
|
394
|
+
**Side effects are replaced for local graph runs.** The tool derives the target's
|
|
395
|
+
reachable side-effect tools and builds a throwaway executor with recorder tools
|
|
396
|
+
under those names. Read-only tools and the other graph collaborators are shared.
|
|
397
|
+
If side effects are reachable but no graph was supplied, it refuses the run.
|
|
398
|
+
This substitution prevents real writes; it does not prove a backend mutation.
|
|
280
399
|
|
|
281
400
|
**Budget**: the persona model + judge model calls are the cost of running the eval,
|
|
282
401
|
charged to the **calling** agent's own turn — never the target's (the target's own
|
data/docs/FACTS.md
CHANGED
|
@@ -38,6 +38,10 @@ decides; the engine never applies its own proposal.
|
|
|
38
38
|
5. Approved facts join the customer's memory cell and are injected by the
|
|
39
39
|
Memory provider on the next turn of any session of that customer.
|
|
40
40
|
|
|
41
|
+
The extraction transcript contains only nonblank `user` and `assistant` text,
|
|
42
|
+
with original message indexes preserved. Tool payloads are excluded regardless
|
|
43
|
+
of `fencing`; an assistant's repetition of tool text can still be included.
|
|
44
|
+
|
|
41
45
|
## Enabling it — the `distill:` block
|
|
42
46
|
|
|
43
47
|
Distillation is pack data on the agent, exactly like `refinement:` or
|
|
@@ -66,12 +70,14 @@ The engine never writes store vocabulary.
|
|
|
66
70
|
|
|
67
71
|
The **Facts** page (operate group, next to Follow-ups) shows:
|
|
68
72
|
|
|
69
|
-
- **Pending** — oldest first, because evidence ages.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
- **Pending** — oldest first, because evidence ages. Select a fact to review its
|
|
74
|
+
proposed value, model confidence and conversation evidence. Long excerpts scroll
|
|
75
|
+
within the evidence panel; **Open conversation** shows their source. Keys and
|
|
76
|
+
scope/session IDs live under **Technical details**. **Approve & save to memory**
|
|
77
|
+
saves the value; **Reject or dismiss** expands the alternatives, including an
|
|
78
|
+
optional rejection reason. Both prevent the same key and value from being proposed
|
|
79
|
+
again. Decisions refresh the queue and open the next pending fact in the selected
|
|
80
|
+
agent/scope.
|
|
75
81
|
- **Stale** — the CAS-lost re-present (below): the proposed value struck
|
|
76
82
|
through next to the operator's current value, resolved by dismissal.
|
|
77
83
|
- **Recent** — every resolved proposal, most recent first, with operator and
|
data/docs/HARVEST.md
CHANGED
|
@@ -35,8 +35,10 @@ A mine reads ONLY durable, finished data — never an in-flight turn, never a
|
|
|
35
35
|
live prompt prefix (the "fork" is structural: the mining writes nothing to the
|
|
36
36
|
sessions it read, so a customer turn's cache is untouched by construction). The
|
|
37
37
|
miner gets the transcript slices (masked), the session's evidence-ledger ids,
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
the agent's current skill names and the candidates still open (name and
|
|
39
|
+
description), so it does not re-propose an idea under another name.
|
|
40
|
+
Simulator and eval sessions (`sim-…`, `eval-…`) are never mined: they are
|
|
41
|
+
not customers. Then the filters, each drop counted and logged:
|
|
40
42
|
|
|
41
43
|
1. **The negative list** — the versioned rules file (seed, imported
|
|
42
44
|
per store by `insika harvest:negative import --agent ID --file F`) and the
|
|
@@ -111,6 +113,9 @@ back, re-promoted is three readable rows.
|
|
|
111
113
|
- `insika harvest:criterion check --file F` — strict-load the frozen conversion criterion (the hook before any promotion).
|
|
112
114
|
- The **Harvest page** in the Studio: the human's inbox (each candidate with
|
|
113
115
|
its evidence excerpt, the eval report, the conversion card, promote/reject),
|
|
114
|
-
the gated-but-blocked rows with the named ruler hole
|
|
116
|
+
the gated-but-blocked rows with the named ruler hole and a "Run checks
|
|
117
|
+
again" once the hole is fixed, the pending list (every row with the skill
|
|
118
|
+
text, the rationale and a link to its origin conversation), a notice when
|
|
119
|
+
the loop skips the agent (harvest off, or no grounding matcher), the
|
|
115
120
|
append-only promoted log with the rollback mirror, the negative list with
|
|
116
121
|
per-rule rejection counts, and the criterion block read-only.
|
data/docs/KNOWLEDGE.md
CHANGED
|
@@ -27,6 +27,13 @@ and an operator can see, edit and resolve all of it in the Studio. Only the
|
|
|
27
27
|
optional FTS5 index remains, deferred with a measured trigger — see
|
|
28
28
|
[What's not here yet](#whats-not-here-yet).
|
|
29
29
|
|
|
30
|
+
Extraction reads only nonblank `user` and `assistant` text, retaining original
|
|
31
|
+
message indexes. Direct tool payloads are excluded regardless of `fencing`;
|
|
32
|
+
assistant paraphrases can still reach the extractor. The names and descriptions
|
|
33
|
+
in the injected `<knowledge>` block are sanitized when
|
|
34
|
+
[fencing](AGENTS.md#fencing--third-party-text-is-data-never-instructions) is on.
|
|
35
|
+
The full body returned by `load_knowledge` is not fenced.
|
|
36
|
+
|
|
30
37
|
## The concept format
|
|
31
38
|
|
|
32
39
|
One concept is one record — a markdown document with a YAML frontmatter
|
|
@@ -88,6 +95,13 @@ end
|
|
|
88
95
|
recalling (write-only, inspected by hand), or recall without learning
|
|
89
96
|
(curate every concept by hand in the Studio, `provenance: policy`).
|
|
90
97
|
|
|
98
|
+
The same settings are available in **Studio → Agents → select an agent →
|
|
99
|
+
Configuration → Memory & knowledge**: learn from conversations, use knowledge in
|
|
100
|
+
replies, concepts per turn, concept types, learning model and instructions.
|
|
101
|
+
Knowledge reranking has its own switch and settings, independent of memory
|
|
102
|
+
reranking, and defaults to off. Options authored in a pack but not exposed in the
|
|
103
|
+
form, such as `index`, are preserved when saving.
|
|
104
|
+
|
|
91
105
|
## The write path
|
|
92
106
|
|
|
93
107
|
After a turn completes, off the turn's critical path (the customer already has
|
data/docs/MEDIA.md
CHANGED
|
@@ -39,6 +39,8 @@ curl -X POST /v1/messages?stream=false -H "Authorization: Bearer $TOKEN" \
|
|
|
39
39
|
the usage flows like any ask. The first image URL is also
|
|
40
40
|
`{{ctx.image_url}}` for data tools — photo analysis outside the prompt, the
|
|
41
41
|
tool's own egress applying when it fetches.
|
|
42
|
+
DeepSeek images require RubyLLM's [image-attachment fix](https://github.com/crmne/ruby_llm/pull/972),
|
|
43
|
+
included in this deployment's pinned revision, and a vision-capable model.
|
|
42
44
|
- **Documents** (a prescription, a recipe, an invoice — most often a PDF)
|
|
43
45
|
attach the same way images do; the first document URL is
|
|
44
46
|
`{{ctx.document_url}}`. A model without document support fails the ask at
|
|
@@ -117,8 +119,10 @@ end
|
|
|
117
119
|
- **Seams, not magic.** The generator is injectable per kind (specs stub it);
|
|
118
120
|
the defaults are lazy: images via RubyLLM (paint), speech via a thin POST to
|
|
119
121
|
the OpenAI-compatible `/audio/speech` endpoint using the same provider
|
|
120
|
-
config the chat uses
|
|
121
|
-
|
|
122
|
+
config the chat uses. RubyLLM 2 has a speech API, but its inspected native
|
|
123
|
+
transport buffers non-audio/error responses without yielding chunks. Insika
|
|
124
|
+
retains its bounded HTTP transport until native limits cover those responses.
|
|
125
|
+
A generated part over 8 MB refuses loudly, never silently truncates.
|
|
122
126
|
- **Not here:** what the generated image *means* — a fitting room, a product
|
|
123
127
|
mockup — is a skill on top. The engine transports bytes and cost.
|
|
124
128
|
|
data/docs/OBSERVABILITY.md
CHANGED
|
@@ -5,7 +5,44 @@ nav_order: 1
|
|
|
5
5
|
permalink: /observability/
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# Observability — OpenTelemetry
|
|
8
|
+
# Observability — native model diagnostics and OpenTelemetry
|
|
9
|
+
|
|
10
|
+
## Studio Models
|
|
11
|
+
|
|
12
|
+
Open the operator-authenticated `/studio/models` page to filter native
|
|
13
|
+
requests by the last 24 hours, 7 days, or 30 days, agent, provider, and model. It shows
|
|
14
|
+
request volume, failures, retries, reported USD cost and unknown-cost coverage,
|
|
15
|
+
p50/p90/p95 latency, separate input/output/cache-read/cache-write/thinking token
|
|
16
|
+
subtotals, and the 20 slowest measured requests linked to their tasks.
|
|
17
|
+
|
|
18
|
+
Charts show request volume, reported cost, and p50/p90/p95 latency over time,
|
|
19
|
+
plus a ranked cost comparison by model. They share the page filters. The 24-hour
|
|
20
|
+
view uses hourly intervals, 7 days uses six-hour intervals, and 30 days uses
|
|
21
|
+
daily intervals, all in UTC. Each chart has an expandable table of exact values.
|
|
22
|
+
Missing cost or duration leaves a gap; no-call intervals have zero requests.
|
|
23
|
+
The page is a snapshot: apply the filters again to refresh it.
|
|
24
|
+
|
|
25
|
+
Request latency includes native retries. Retries count usage attempts beyond the
|
|
26
|
+
first within a request, not Insika task executions. Percentiles use completed
|
|
27
|
+
requests with measured durations. Cost and token values sum only reported usage;
|
|
28
|
+
when coverage is incomplete, they are subtotals, and a dash means no value was
|
|
29
|
+
reported. An unknown-cost failed attempt followed by a priced success leaves
|
|
30
|
+
partial cost coverage even when the subtotal is known.
|
|
31
|
+
|
|
32
|
+
Every turn also leaves one row (agent, model, provider, status and its
|
|
33
|
+
windows), whatever `INSIKA_TURN_TIMING` says. A failed turn keeps its row with
|
|
34
|
+
the stage that failed and a total that runs to the failure, so timeouts stay in
|
|
35
|
+
the p95 and the page shows the turn failure rate by stage; that flag only decides what the
|
|
36
|
+
response exposes. **Turn latency** shows p50/p95 of ttft (the provider's first
|
|
37
|
+
token), total, first balloon (what a channel customer waits), queue (debounce
|
|
38
|
+
and FIFO wait before the turn starts), prep and tools. **Tool health** lists
|
|
39
|
+
every tool call in those turns: calls, ok %, p50/p95 ms. A call is a failure
|
|
40
|
+
when its result carries `error`; tools ms sums calls, so parallel calls count
|
|
41
|
+
more than their wall time.
|
|
42
|
+
|
|
43
|
+
The page reads content-free summaries stored independently of the capped task
|
|
44
|
+
trace. History starts when the summaries are deployed; earlier requests are not
|
|
45
|
+
backfilled. Task deletion, retention, and tenant purge remove their summaries.
|
|
9
46
|
|
|
10
47
|
Insika already has an observability spine: the **event stream**. Every turn emits
|
|
11
48
|
structured events (`task_started`, `tool_call`/`tool_result`, `data_tool_call`,
|
|
@@ -25,15 +62,17 @@ authoring writes (`:golden_written`, `:agent_file_written`, …), queue bookkeep
|
|
|
25
62
|
channel delivery (`:channel_delivered` — see [Channels](CHANNELS.md))
|
|
26
63
|
travel the same stream and are **ignored** by the bridge: they open no span and
|
|
27
64
|
touch no instrument, because they are not part of a turn's latency or cost. Any
|
|
28
|
-
other subscriber still sees them.
|
|
29
|
-
|
|
30
|
-
(no span: the intervention is a fact about the turn, not a timed operation).
|
|
65
|
+
other subscriber still sees them. Additional events feed counters without opening
|
|
66
|
+
spans: `:tool_loop_intervened`, `:tool_blocked` and `:context_compacted`.
|
|
31
67
|
|
|
32
68
|
They are worth subscribing to even so, because each is the ONLY record of
|
|
33
69
|
something that left no task of its own behind:
|
|
34
70
|
|
|
35
71
|
| Event | Data | What it answers |
|
|
36
72
|
|---|---|---|
|
|
73
|
+
| `:tool_blocked` | `name`, `gate`, `param` | a provenance gate refused a call before execution; no rejected value in this event |
|
|
74
|
+
| `:ui` | `component`, `title`, `items`, `count`, `dropped` | the presentation selection, including customer-facing card content; see [API](API.md#tool-and-presentation-sse-events) |
|
|
75
|
+
| `:session_seeded` | `session_id`, `tenant`, `keys` | an eval snapshot was loaded; no snapshot contents |
|
|
37
76
|
| `:turn_coalesced` | `task_id`, `merged`, `arrivals[]` | the fragments a customer typed in a row arrived as separate messages, and when |
|
|
38
77
|
| `:turn_steered` | `task_id`, `count`, `total` | a message arrived mid-run and was appended to the turn in flight |
|
|
39
78
|
| `:turn_steer_released` | `task_id`, `released_as`, `count` | the run could not absorb it, so it became the turn `released_as` |
|
|
@@ -60,31 +99,52 @@ something that left no task of its own behind:
|
|
|
60
99
|
| `:knowledge_deleted` | `name`, `agent` | an operator removed a concept from the Studio |
|
|
61
100
|
| `:knowledge_retrieved` | `name`, `agent` | the model called `load_knowledge` — the adoption signal (retrieval calls per conversation), not the `<knowledge>` injection itself |
|
|
62
101
|
| `:knowledge_backfilled` | `agent`, `sessions`, `concepts`, `conflicts`, `dropped{}` | the recovery re-scan (`insika knowledge:backfill`) finished replaying an agent's stored sessions |
|
|
102
|
+
| `:retrieval_reranked` | `provider`, `candidate_count`, `selected_count` | an opt-in memory or knowledge rerank selected candidates; no candidate text |
|
|
103
|
+
| `:provider_warning` | `provider`, `message` | a rerank failed or returned invalid indexes; fallback context remains available |
|
|
63
104
|
|
|
64
105
|
`delivery_failed` and `breaker_open` are the two the operator config is pointed at
|
|
65
106
|
(`alerts.webhook` on the profile): each only fires when something durable did
|
|
66
107
|
not land. `:ttft` is additive debug, absent unless `INSIKA_TURN_TIMING` is set.
|
|
67
108
|
|
|
109
|
+
Rerank requests have `operation: "rerank"` in native model diagnostics. Their
|
|
110
|
+
reported cost is tracked separately from chat and included once in the total.
|
|
111
|
+
If any rerank request lacks usage, the rerank and total cost remain unknown.
|
|
112
|
+
Reported native rerank tokens, including reported failed attempts, count once
|
|
113
|
+
against agent token ceilings and calendar budgets, separately from chat usage.
|
|
114
|
+
Unknown token fields stay unknown. Diagnostics carry the task and captured turn.
|
|
115
|
+
Candidate and selected counts measure narrowing, not recall: records excluded
|
|
116
|
+
by the lexical index or recent-note window cannot appear in those counts.
|
|
117
|
+
|
|
68
118
|
`:channel_delivered` is the one worth alerting on: a turn can be `:task_completed`
|
|
69
119
|
and correct while the customer got nothing, because delivery is a separate,
|
|
70
120
|
retried, out-of-band step. `status: "failed"` means the reply is sitting in the
|
|
71
121
|
outbox and the customer is still waiting.
|
|
72
122
|
|
|
73
|
-
|
|
74
|
-
|
|
123
|
+
Operational audit events use metadata rather than transcript bodies. Presentation
|
|
124
|
+
`:ui` events intentionally contain customer-facing card content; tool-call events
|
|
125
|
+
can contain arguments. Do not treat the whole event stream as content-free.
|
|
75
126
|
|
|
76
127
|
The bridge speaks the standard the market already runs on: point any OTLP backend
|
|
77
128
|
at Insika and a real turn shows up as a full trace, next to counters and histograms
|
|
78
129
|
you can chart without touching a span.
|
|
79
130
|
|
|
80
|
-
**
|
|
131
|
+
**The OpenTelemetry section is a convention, not an integration.** Insika ships no OTEL dashboard, no
|
|
81
132
|
backend config, no vendor file. It ships a stable set of attribute and instrument
|
|
82
133
|
names, and the recipes below tell you what to chart against them — in whatever you
|
|
83
134
|
already run.
|
|
84
135
|
|
|
136
|
+
### Blocked tool calls
|
|
137
|
+
|
|
138
|
+
`tool_blocked` carries `name`, `gate`, and `param`, with task/session correlation
|
|
139
|
+
in event metadata. It never carries the rejected value. The `insika.tool.blocked`
|
|
140
|
+
counter (unit `{call}`) uses the turn's agent/tenant/command labels plus
|
|
141
|
+
`insika.tool` and `insika.gate`. Session traces keep the `gate` field alongside the
|
|
142
|
+
masked result; `insika tools:report` lists blocked calls separately from errors.
|
|
143
|
+
|
|
85
144
|
## Contents
|
|
86
145
|
|
|
87
146
|
- [Turning it on](#turning-it-on-opt-in-parity-when-off)
|
|
147
|
+
- [Native model diagnostics](#native-model-diagnostics)
|
|
88
148
|
- [Traces: the span reference](#traces-the-span-reference)
|
|
89
149
|
- [Metrics: the instrument reference](#metrics-the-instrument-reference)
|
|
90
150
|
- [Attribute reference](#attribute-reference)
|
|
@@ -112,12 +172,43 @@ stay on; `OTEL_METRIC_EXPORT_INTERVAL` (ms) sets the export period. If the metri
|
|
|
112
172
|
SDK is not in the bundle at all, the bridge degrades to traces only rather than
|
|
113
173
|
failing to boot.
|
|
114
174
|
|
|
115
|
-
**Off (the default):** `Insika::Telemetry.setup` returns `nil
|
|
116
|
-
**never loaded** (lazy `require`, like the LLM client)
|
|
117
|
-
|
|
175
|
+
**Off (the default):** `Insika::Telemetry.setup` returns `nil` and the OTEL gems are
|
|
176
|
+
**never loaded** (lazy `require`, like the LLM client). No OTEL spans or metrics
|
|
177
|
+
are exported; native model diagnostics remain available in Studio. The lazy
|
|
178
|
+
load is enforced by a test
|
|
118
179
|
(`spec/insika/load_guard_spec.rb`: "require insika does not load
|
|
119
180
|
OpenTelemetry").
|
|
120
181
|
|
|
182
|
+
A failed export (a 404 from an ingest that wants a header, a wrong key) is
|
|
183
|
+
logged to stderr as `ERROR -- otel: OpenTelemetry error: OTLP exporter received
|
|
184
|
+
http.code=…`. `OTEL_EXPORTER_OTLP_HEADERS` is `key=value[,key=value]` and each
|
|
185
|
+
value is URL-decoded: a `+` in a token becomes a space, so write it as `%2B`
|
|
186
|
+
(and `%` as `%25`).
|
|
187
|
+
|
|
188
|
+
## Native model diagnostics
|
|
189
|
+
|
|
190
|
+
The Studio task page shows `llm_request` and `llm_usage` rows from RubyLLM. A
|
|
191
|
+
request is one logical model operation, including RubyLLM's transport retries;
|
|
192
|
+
each attempt has its own usage row. A successful task can therefore show a failed
|
|
193
|
+
attempt followed by a successful attempt under one request. These are not extra
|
|
194
|
+
task executions. Each request has a locally generated `request_id` that links its
|
|
195
|
+
rows; it is not the provider's HTTP request ID. The rows show operation,
|
|
196
|
+
provider, model, outcome, and five token buckets (input, output, cache read,
|
|
197
|
+
cache write, thinking). Only the request has a measured duration. Attempts have
|
|
198
|
+
no measured duration. Cost is RubyLLM's reported attempt cost, or the
|
|
199
|
+
`INSIKA_MODEL_PRICING` estimate when RubyLLM reports none: unknown
|
|
200
|
+
remains an em dash, while a reported zero remains zero. These diagnostic values
|
|
201
|
+
do not add to turn usage, `insika.tokens`, or `insika.cost`.
|
|
202
|
+
|
|
203
|
+
The diagnostic store retains the latest 200 events per task. Studio marks a
|
|
204
|
+
truncated tail; deleting the task deletes its diagnostics. Insika stores and
|
|
205
|
+
exports only allowlisted scalar fields: event type, timestamp, turn number,
|
|
206
|
+
operation, provider, model, outcome, request ID, duration, exception class,
|
|
207
|
+
token counts, and cost. It excludes prompts, messages, tool arguments, responses,
|
|
208
|
+
URLs, exception messages, and arbitrary RubyLLM payload fields. This restriction
|
|
209
|
+
applies to Insika's diagnostics; an application-supplied RubyLLM instrumenter
|
|
210
|
+
still receives RubyLLM's original payload under the host's own policy.
|
|
211
|
+
|
|
121
212
|
## Traces: the span reference
|
|
122
213
|
|
|
123
214
|
One span per **turn**, with **child spans** per tool, correlated by `task_id`.
|
|
@@ -128,16 +219,29 @@ Latency is the span duration, reconstructed from the events' real `at` timestamp
|
|
|
128
219
|
| `insika.turn` | one per turn, opened on `task_started`, closed on the terminal event | root |
|
|
129
220
|
| `insika.tool` | one per tool call, `tool_call` → `tool_result` (FIFO-correlated) | `insika.turn` |
|
|
130
221
|
| `insika.data_tool` | one per data-tool call — point-in-time (the engine emits a single event) | `insika.turn` |
|
|
222
|
+
| `insika.llm.request` | one per retained native request with measured duration, exported when the turn ends | `insika.turn` |
|
|
131
223
|
|
|
132
224
|
A turn that ends with `task_failed` also carries the OTEL **error status**, with
|
|
133
|
-
the
|
|
225
|
+
the exception class (or `Task failed` when the class is unavailable), never the
|
|
226
|
+
exception message. A failed native request uses the same class-only rule.
|
|
227
|
+
|
|
228
|
+
Request spans carry `insika.llm.request_id`, `insika.model`, `insika.provider`,
|
|
229
|
+
`insika.operation`, `insika.status`, `insika.llm.duration_ms`,
|
|
230
|
+
`insika.llm.attempts`, and `insika.llm.failures`. Each retained attempt adds
|
|
231
|
+
`insika.llm.attempt.N.status` and, when known,
|
|
232
|
+
`insika.llm.attempt.N.input_tokens`, `output_tokens`, `cache_read_tokens`,
|
|
233
|
+
`cache_write_tokens`, `thinking_tokens`, and `cost`. The `N` starts at 1. Unknown
|
|
234
|
+
values are omitted. Request spans wait until the turn ends so usage emitted after
|
|
235
|
+
request completion is included. When the 200-event buffer overflows,
|
|
236
|
+
`insika.llm.truncated=true` appears on the turn and request spans: their attempt
|
|
237
|
+
counts describe only the retained tail.
|
|
134
238
|
|
|
135
239
|
## Metrics: the instrument reference
|
|
136
240
|
|
|
137
241
|
The same events feed instruments, so volume, latency, tokens and cost are chartable
|
|
138
242
|
**without aggregating spans** — which not every backend does, and none does cheaply
|
|
139
|
-
at retention.
|
|
140
|
-
|
|
243
|
+
at retention. Turn metrics are recorded on the *terminal* event, when its outcome
|
|
244
|
+
is known. Native attempt counters are recorded as usage arrives.
|
|
141
245
|
|
|
142
246
|
| Instrument | Type | Unit | Recorded when |
|
|
143
247
|
|------------|------|------|----------------|
|
|
@@ -145,11 +249,20 @@ knows its outcome.
|
|
|
145
249
|
| `insika.turn.duration` | histogram | `s` | same, when both timestamps are known |
|
|
146
250
|
| `insika.tokens` | counter | `{token}` | the turn reported usage |
|
|
147
251
|
| `insika.cost` | counter | `{USD}` | the turn's model is priced (see below) |
|
|
252
|
+
| `insika.tool.blocked` | counter | `{call}` | a `tool_blocked` gate refusal |
|
|
253
|
+
| `insika.confirmation` | counter | `{hold}` | a customer confirmation is requested, confirmed, cancelled or expires (`insika.outcome`) |
|
|
148
254
|
| `insika.tool.calls` | counter | `{call}` | a tool call completes |
|
|
149
255
|
| `insika.tool.duration` | histogram | `s` | a `tool_call`/`tool_result` pair completes |
|
|
150
256
|
| `insika.cache.hit_rate` | histogram | `%` | a turn reported billed prompt tokens (see below) |
|
|
151
257
|
| `insika.tool.loop_intervened` | counter | `{intervention}` | the loop detector delivered its one-shot warning |
|
|
152
258
|
| `insika.context.compacted` | counter | `{compaction}` | an in-session compaction was persisted (RFC-0044) |
|
|
259
|
+
| `insika.llm.attempts` | counter | `{attempt}` | each native usage event, including retries |
|
|
260
|
+
| `insika.llm.failures` | counter | `{attempt}` | each native usage event with failed status |
|
|
261
|
+
|
|
262
|
+
The native attempt counters count every received attempt, even when the span
|
|
263
|
+
buffer has discarded earlier rows. They use the turn's agent/tenant/command
|
|
264
|
+
labels plus `insika.model`, `insika.provider`, `insika.operation`, and
|
|
265
|
+
`insika.status`; neither task nor request ID becomes a metric label.
|
|
153
266
|
|
|
154
267
|
`insika.tool.duration` is deliberately **not** recorded for data-tools: those are a
|
|
155
268
|
single point-in-time event, so there is no measured duration to report. A tool left
|
|
@@ -189,10 +302,11 @@ metric attribute with per-turn cardinality is how you destroy a metrics backend.
|
|
|
189
302
|
| `insika.status` | string | `turn` | turn instruments | `ok` / `error` / `cancelled` / `abandoned` |
|
|
190
303
|
| `insika.model` | string | `turn` | all except tool | model id the provider reported |
|
|
191
304
|
| `insika.model_source` | string | `turn` | — | which config layer resolved the model (chat / agent / model / global) |
|
|
192
|
-
| `insika.
|
|
305
|
+
| `insika.provider` | string | `turn` | — | provider of the turn's final model selection (the fallback's, when one answered) |
|
|
306
|
+
| `insika.tokens.input` | int | `turn` | — | non-cached input tokens |
|
|
193
307
|
| `insika.tokens.output` | int | `turn` | — | output tokens |
|
|
194
|
-
| `insika.tokens.total` | int | `turn` | — | input + output |
|
|
195
|
-
| `insika.tokens.cached` | int | `turn` | — | cache **reads**,
|
|
308
|
+
| `insika.tokens.total` | int | `turn` | — | non-cached input + output subtotal; cache buckets are separate |
|
|
309
|
+
| `insika.tokens.cached` | int | `turn` | — | cache **reads**, separate from `tokens.input` |
|
|
196
310
|
| `insika.tokens.cache_creation` | int | `turn` | — | cache **writes**, *not* inside `tokens.input` |
|
|
197
311
|
| `insika.token.type` | string | — | `insika.tokens` | `input` / `output` / `cached` / `cache_creation` |
|
|
198
312
|
| `insika.cost.usd` | double | `turn` | — | estimated cost of the turn (span attribute; the metric is the `insika.cost` counter) |
|
|
@@ -219,12 +333,27 @@ attributes are there for it.
|
|
|
219
333
|
|
|
220
334
|
## Estimated cost
|
|
221
335
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
336
|
+
RubyLLM supplies attempt-time usage and cost. Insika aggregates native chat usage
|
|
337
|
+
across tool rounds and retries, including recorded usage before a failure, and
|
|
338
|
+
exports the recorded `cost_usd`. Unknown costs remain unknown, not zero.
|
|
339
|
+
Insika maintains no competing provider price catalog.
|
|
340
|
+
|
|
341
|
+
Internal token buckets are disjoint. The `total_tokens` field retains its historical
|
|
342
|
+
non-cached input/output subtotal; budgets and evals add the cache buckets once.
|
|
343
|
+
The Responses HTTP adapter projects cache-inclusive `input_tokens` and `total_tokens`.
|
|
225
344
|
|
|
226
|
-
|
|
227
|
-
|
|
345
|
+
RubyLLM prices from its model catalog. Insika keeps that catalog in its store
|
|
346
|
+
and refreshes it every `INSIKA_MODEL_REFRESH_HOURS` (default 24; 0 keeps the
|
|
347
|
+
gem's bundled catalog): one worker downloads it, the others reload it, and a
|
|
348
|
+
restart starts from the last download instead of the catalog shipped with the gem.
|
|
349
|
+
|
|
350
|
+
`INSIKA_MODEL_PRICING` optionally fills in cost where RubyLLM reports none (a
|
|
351
|
+
model missing from its registry, a negotiated contract) — everywhere: Studio
|
|
352
|
+
Models, task traces, turn usage and telemetry. RubyLLM's cost always wins, so the
|
|
353
|
+
table never goes stale against a priced model. Rates are a JSON object of model
|
|
354
|
+
id to **USD per million tokens**.
|
|
355
|
+
Each request is priced under its own model, so a turn that ran a failed primary,
|
|
356
|
+
a fallback and the router costs each at its own rate:
|
|
228
357
|
|
|
229
358
|
```bash
|
|
230
359
|
INSIKA_MODEL_PRICING='{
|
|
@@ -237,13 +366,17 @@ INSIKA_MODEL_PRICING='{
|
|
|
237
366
|
`provider/` prefix — `deepseek/deepseek-v4-flash` and `deepseek-v4-flash` both hit the
|
|
238
367
|
same entry.
|
|
239
368
|
- `input` / `output` are required (one of them is enough for the entry to load).
|
|
240
|
-
- `cached_input`, when given, bills cache **reads** at that rate
|
|
241
|
-
|
|
369
|
+
- `cached_input`, when given, bills cache **reads** separately at that rate.
|
|
370
|
+
Omit it and cache reads use the input rate; they are never subtracted from fresh input.
|
|
242
371
|
- `cache_write`, when given, bills cache **creation** tokens at that rate. Omit it
|
|
243
372
|
and they are billed at the input rate.
|
|
244
|
-
-
|
|
245
|
-
|
|
246
|
-
- A malformed table
|
|
373
|
+
- A model RubyLLM prices never reads the table. If neither supplies a cost,
|
|
374
|
+
there is no attribute or metric point. A missing price is not zero.
|
|
375
|
+
- A malformed table disables the override. Telemetry config cannot stop a boot.
|
|
376
|
+
|
|
377
|
+
Routing (`operation: "routing"`) and summarizing compaction
|
|
378
|
+
(`operation: "compaction"`) requests are recorded like the chat's, so they show
|
|
379
|
+
on Studio Models next to it.
|
|
247
380
|
|
|
248
381
|
The number is an **estimate for trend and attribution**, not a bill. Reconcile
|
|
249
382
|
against your provider's invoice, never the other way round.
|
|
@@ -291,7 +424,10 @@ climbing while `input` stays flat.
|
|
|
291
424
|
`insika.tokens` filtered to `insika.token.type="cached"` over the same counter
|
|
292
425
|
filtered to `input`. This is the number that moves your bill. For the per-turn
|
|
293
426
|
distribution (does every turn hit, or do fleet averages hide cold agents?), chart
|
|
294
|
-
`insika.cache.hit_rate` — p50 by `insika.agent
|
|
427
|
+
`insika.cache.hit_rate` — p50 by `insika.agent`. Compare with that agent's baseline:
|
|
428
|
+
cache eligibility, expiry and changing history affect the ratio. The
|
|
429
|
+
[identity-prefix fingerprint](CONTEXT.md#the-observable-cache-fingerprints-and-the-invalidation-reason)
|
|
430
|
+
explains identity/tool-schema changes; volatile category digests are separate.
|
|
295
431
|
|
|
296
432
|
**Loop interventions**
|
|
297
433
|
`insika.tool.loop_intervened`, rate, grouped by `insika.agent` and `insika.tool`.
|
|
@@ -374,7 +510,7 @@ Set the OTEL envs on the deployment and every worker exports to your collector:
|
|
|
374
510
|
INSIKA_OTEL=1
|
|
375
511
|
OTEL_EXPORTER_OTLP_ENDPOINT=https://<your-collector>:4318
|
|
376
512
|
OTEL_SERVICE_NAME=insika # optional; names the service
|
|
377
|
-
INSIKA_MODEL_PRICING='{...}' # optional
|
|
513
|
+
INSIKA_MODEL_PRICING='{...}' # optional negotiated-rate override for telemetry
|
|
378
514
|
# plus any standard OTEL_EXPORTER_OTLP_HEADERS your backend needs
|
|
379
515
|
```
|
|
380
516
|
|