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
|
@@ -1,44 +1,59 @@
|
|
|
1
|
-
<%# Tools — miller columns:
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
<%# Tools — the catalog, as miller columns: every tool in the master (grouped by
|
|
2
|
+
origin, searchable, with how many agents can call it) | the detail in a
|
|
3
|
+
turbo-frame: a data tool's editor, or a read-only card for code and MCP
|
|
4
|
+
tools. Which agent may call what lives on the agent (its Tools tab).
|
|
5
|
+
@detail: nil (landing) | :edit (data-tool editor) | :info (read-only card). %>
|
|
6
6
|
<% frame_only = defined?(frame_only) && frame_only %>
|
|
7
7
|
<% unless frame_only %>
|
|
8
|
+
<header class="page-head">
|
|
9
|
+
<h1>Tools<span class="sub">Every tool agents can call — edit HTTP tools, see who uses each one</span></h1>
|
|
10
|
+
</header>
|
|
8
11
|
<div class="drill" data-controller="list-filter">
|
|
9
12
|
<aside class="drill-pane drill-master">
|
|
10
13
|
<div class="drill-pane-head">
|
|
11
|
-
<div class="title"><strong>Tools <span class="count"><%= @tools.size %></span></strong></div>
|
|
12
|
-
<a class="btn primary btn-sm" href="/studio/tools/def/new"
|
|
14
|
+
<div class="title"><strong>Tools <span class="count" data-list-filter-target="count"><%= @tools.size %></span></strong></div>
|
|
15
|
+
<a class="btn primary btn-sm" href="/studio/tools/def/new" data-turbo-frame="tool-detail" data-turbo-action="advance">+ New tool</a>
|
|
13
16
|
</div>
|
|
14
17
|
<div class="drill-search">
|
|
15
|
-
<input type="search" class="filter-input" placeholder="
|
|
18
|
+
<input type="search" class="filter-input" placeholder="Search tools…" aria-label="Search tools"
|
|
16
19
|
data-list-filter-target="query" data-action="input->list-filter#filter keydown->list-filter#clear">
|
|
20
|
+
<label class="switch" title="show only tools no agent can call">
|
|
21
|
+
<input type="checkbox" id="only-unused">
|
|
22
|
+
<span class="switch-track" aria-hidden="true"></span>
|
|
23
|
+
<span class="switch-label">only unused</span>
|
|
24
|
+
</label>
|
|
17
25
|
</div>
|
|
18
|
-
<nav class="drill-pane-body drill-list" aria-label="tools
|
|
19
|
-
<%
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
<%
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
<nav class="drill-pane-body drill-list" aria-label="tools" data-controller="active-link">
|
|
27
|
+
<% @tool_groups.each do |label, tools| %>
|
|
28
|
+
<details class="drill-group" data-list-filter-target="group"<%== " open" unless label.start_with?("MCP") %>>
|
|
29
|
+
<summary class="drill-group-label"><%= label %> <span class="count"><%= tools.size %></span></summary>
|
|
30
|
+
<% tools.each do |t| %>
|
|
31
|
+
<% data = @data_tool_names.include?(t.name) %>
|
|
32
|
+
<% users = @tool_reach[t.name].size %>
|
|
33
|
+
<a class="drill-item drill-item-sm<%== ' active' if @selected == t.name %>"
|
|
34
|
+
href="/studio/tools/<%= data ? "def" : "info" %>/<%= Rack::Utils.escape(t.name) %>"<%== " data-unused" if users.zero? %>
|
|
35
|
+
data-turbo-frame="tool-detail" data-turbo-action="advance"
|
|
36
|
+
data-list-filter-target="item" data-filter-text="<%= "#{t.name} #{label}" %>"
|
|
37
|
+
data-active-link-target="item" data-action="click->active-link#select" title="<%= t.description %>">
|
|
38
|
+
<div class="drill-item-name"><span class="txt"><%= t.name %></span><% if data %><span class="pill plain">data</span><% end %><span class="pill <%= users.zero? ? "warn" : "plain" %>"><%= users %> <%= users == 1 ? "agent" : "agents" %></span></div>
|
|
39
|
+
</a>
|
|
40
|
+
<% end %>
|
|
41
|
+
</details>
|
|
28
42
|
<% end %>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
<% if @dropped_tool_names.any? %>
|
|
44
|
+
<details class="drill-group" data-list-filter-target="group" open>
|
|
45
|
+
<summary class="drill-group-label">Invalid definitions <span class="count"><%= @dropped_tool_names.size %></span></summary>
|
|
46
|
+
<% @dropped_tool_names.sort.each do |name| %>
|
|
47
|
+
<a class="drill-item drill-item-sm" href="/studio/tools/def/<%= Rack::Utils.escape(name) %>"
|
|
48
|
+
data-turbo-frame="tool-detail" data-turbo-action="advance"
|
|
49
|
+
data-list-filter-target="item" data-filter-text="<%= name %>"
|
|
50
|
+
title="its definition is invalid — no agent can call it until it is fixed">
|
|
51
|
+
<div class="drill-item-name"><span class="txt"><%= name %></span><span class="pill locked">dropped</span></div>
|
|
52
|
+
</a>
|
|
53
|
+
<% end %>
|
|
54
|
+
</details>
|
|
40
55
|
<% end %>
|
|
41
|
-
<div class="empty" data-list-filter-target="empty" hidden>No
|
|
56
|
+
<div class="empty" data-list-filter-target="empty" hidden>No tools match your search.</div>
|
|
42
57
|
</nav>
|
|
43
58
|
</aside>
|
|
44
59
|
<% end %>
|
|
@@ -47,54 +62,28 @@
|
|
|
47
62
|
<% if flash["error"] %><div class="flash err" role="alert"><%= flash["error"] %></div><% end %>
|
|
48
63
|
<% if flash["notice"] %><div class="flash ok" role="status"><%= flash["notice"] %></div><% end %>
|
|
49
64
|
<% end %>
|
|
50
|
-
<%
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
<% else %>
|
|
58
|
-
<% a = @sel_agent %>
|
|
65
|
+
<% case @detail %>
|
|
66
|
+
<% when :edit %>
|
|
67
|
+
<%== render("_tool_edit") %>
|
|
68
|
+
<% when :info %>
|
|
69
|
+
<% t = @info_tool %>
|
|
59
70
|
<div class="drill-pane-head">
|
|
60
71
|
<div class="title">
|
|
61
|
-
<strong class="mono"><%=
|
|
72
|
+
<strong class="mono"><%= t.name %></strong>
|
|
73
|
+
<span class="count"><%= @info_label %></span>
|
|
62
74
|
</div>
|
|
63
|
-
<button type="submit" form="tools-form" class="btn primary" data-turbo-submits-with="Saving…">Save tools</button>
|
|
64
75
|
</div>
|
|
65
|
-
<div class="drill-pane-body"
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
</
|
|
74
|
-
<
|
|
75
|
-
title="uncheck every tool (deny-all — Save tools to apply)">none</button>
|
|
76
|
+
<div class="drill-pane-body">
|
|
77
|
+
<p><%= t.description %></p>
|
|
78
|
+
<p class="muted"><%= t.plugin.to_s.start_with?("mcp:") ? "Comes from an MCP server; edit the server under MCP." : "Built into the engine; it cannot be edited here." %></p>
|
|
79
|
+
<%== render("_tool_users", locals: { name: t.name }) %>
|
|
80
|
+
</div>
|
|
81
|
+
<% else %>
|
|
82
|
+
<div class="drill-empty">
|
|
83
|
+
<div>
|
|
84
|
+
<h2>Tools</h2>
|
|
85
|
+
<p>Pick a tool to see which agents can call it. HTTP tools open their editor; MCP and built-in tools are read-only. Which tools an agent may call is set on the agent, in its <strong>Tools</strong> tab.</p>
|
|
76
86
|
</div>
|
|
77
|
-
<p class="muted">Checked tools are allowed next turn. A <span class="pill locked">locked</span> tool is on the denylist — deny always wins, so it can't be granted here.</p>
|
|
78
|
-
<form id="tools-form" method="post" action="/studio/tools/<%= Rack::Utils.escape(a.id) %>">
|
|
79
|
-
<%== csrf_tag %>
|
|
80
|
-
<% if @tools.empty? %>
|
|
81
|
-
<p class="muted">No tools registered in the runtime.</p>
|
|
82
|
-
<% else %>
|
|
83
|
-
<div class="check-grid tool-grid<%== " is-open" if a.tools_allow.nil? %>" data-toggle-counter-target="grid">
|
|
84
|
-
<% @tools.each do |t| %>
|
|
85
|
-
<% denied = tool_denied_for?(a, t.name) %>
|
|
86
|
-
<label class="check tool-check<%== " locked" if denied %>" title="<%= denied ? "denied — deny wins" : t.description %>">
|
|
87
|
-
<input type="checkbox" name="tools[]" value="<%= t.name %>"
|
|
88
|
-
<%== " checked" if tool_allowed_for?(a, t.name) && !denied %>
|
|
89
|
-
<%== " disabled" if denied %>
|
|
90
|
-
data-toggle-counter-target="tool" data-locked="<%= denied %>"
|
|
91
|
-
data-action="change->toggle-counter#update">
|
|
92
|
-
<span class="mono"><%= t.name %></span><% if @data_tool_names.include?(t.name) %> <span class="pill plain">data</span><% end %><% if denied %> <span class="pill locked">locked</span><% end %>
|
|
93
|
-
</label>
|
|
94
|
-
<% end %>
|
|
95
|
-
</div>
|
|
96
|
-
<% end %>
|
|
97
|
-
</form>
|
|
98
87
|
</div>
|
|
99
88
|
<% end %>
|
|
100
89
|
</turbo-frame>
|
data/lib/insika/task_actor.rb
CHANGED
|
@@ -41,8 +41,11 @@ module Insika
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# Runs the block on an Async fiber CHILD of the parent. Returns the Task.
|
|
44
|
+
# The fiber yields nil: Async keeps a finished task linked to the siblings that
|
|
45
|
+
# follow it (List#remove! leaves `tail`), so one stale reference to an old turn
|
|
46
|
+
# keeps every later one reachable — with its result, unless the result is nil.
|
|
44
47
|
def run(&turn_block)
|
|
45
|
-
@async_task = @parent.async { turn_block.call(self) }
|
|
48
|
+
@async_task = @parent.async { turn_block.call(self); nil }
|
|
46
49
|
end
|
|
47
50
|
|
|
48
51
|
# Drains the mailbox WITHOUT blocking (boundaries). `:cancel` raises (the top
|
data/lib/insika/task_store.rb
CHANGED
|
@@ -117,6 +117,48 @@ module Insika
|
|
|
117
117
|
to_task(record)
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
+
# -> Task; begin_execution + the move to :running in ONE write and one
|
|
121
|
+
# transaction (the dispatch claim, like transition). Only queued, paused and
|
|
122
|
+
# waiting move to :running; any other status (a recovered orphan already
|
|
123
|
+
# :running) keeps its own, exactly as the two separate calls did.
|
|
124
|
+
def start_execution(id)
|
|
125
|
+
@store.transaction do
|
|
126
|
+
record = fetch!(id)
|
|
127
|
+
raise ArgumentError, "an open Execution already exists on task #{id}" if open_execution(record)
|
|
128
|
+
|
|
129
|
+
record["status"] = "running" if %w[queued paused waiting].include?(record["status"])
|
|
130
|
+
record["executions"] += [{ "attempt" => record["executions"].size + 1, "started_at" => timestamp,
|
|
131
|
+
"finished_at" => nil, "outcome" => nil, "error" => nil }]
|
|
132
|
+
record["updated_at"] = timestamp
|
|
133
|
+
@store.set(SCOPE, key_for(id), record)
|
|
134
|
+
to_task(record)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# -> Task; finish_execution + transition(to: outcome) in ONE write and one
|
|
139
|
+
# transaction. ArgumentError (nothing written) for no open Execution or an
|
|
140
|
+
# invalid transition.
|
|
141
|
+
def complete_execution(id, outcome:)
|
|
142
|
+
target = outcome.to_sym
|
|
143
|
+
raise ArgumentError, "invalid status: #{outcome}" unless STATUSES.include?(target)
|
|
144
|
+
|
|
145
|
+
@store.transaction do
|
|
146
|
+
record = fetch!(id)
|
|
147
|
+
open = open_execution(record)
|
|
148
|
+
raise ArgumentError, "no open Execution on task #{id}" if open.nil?
|
|
149
|
+
|
|
150
|
+
from = record["status"].to_sym
|
|
151
|
+
raise ArgumentError, "invalid transition: #{from} -> #{target}" unless TRANSITIONS.fetch(from).include?(target)
|
|
152
|
+
|
|
153
|
+
open["finished_at"] = timestamp
|
|
154
|
+
open["outcome"] = target.to_s
|
|
155
|
+
record["status"] = target.to_s
|
|
156
|
+
record["updated_at"] = timestamp
|
|
157
|
+
@store.set(SCOPE, key_for(id), record)
|
|
158
|
+
to_task(record)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
120
162
|
# -> Task; closes the current Execution. ArgumentError if none is open.
|
|
121
163
|
# Does NOT touch status (that is transition's job).
|
|
122
164
|
def finish_execution(id, outcome:)
|
|
@@ -189,6 +231,20 @@ module Insika
|
|
|
189
231
|
def queued = with_status(:queued)
|
|
190
232
|
|
|
191
233
|
# -> enumerates ids without the "task:" prefix; without a block returns an Enumerator.
|
|
234
|
+
# -> [Task] every record, from ONE bulk read of the backend (see
|
|
235
|
+
# Store#entries) instead of a read per id.
|
|
236
|
+
def all
|
|
237
|
+
@store.entries(SCOPE, KEY_PREFIX).map { |_, record| to_task(record) }
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# -> [Task] one page, most recently written first (every status change
|
|
241
|
+
# rewrites the task, so this is "most recently updated"). Reads only the page.
|
|
242
|
+
def recent(limit, offset: 0)
|
|
243
|
+
# offset only when paging: a backend written for the 3-argument #recent keeps working
|
|
244
|
+
rows = offset.zero? ? @store.recent(SCOPE, KEY_PREFIX, limit) : @store.recent(SCOPE, KEY_PREFIX, limit, offset)
|
|
245
|
+
rows.map { |_, record| to_task(record) }
|
|
246
|
+
end
|
|
247
|
+
|
|
192
248
|
def each_id
|
|
193
249
|
return enum_for(:each_id) unless block_given?
|
|
194
250
|
|
|
@@ -201,7 +257,17 @@ module Insika
|
|
|
201
257
|
# tasks go with it, terminal tasks only (a live :running task must never
|
|
202
258
|
# be deleted under its own fiber).
|
|
203
259
|
def delete(id)
|
|
204
|
-
@store.
|
|
260
|
+
@store.transaction do
|
|
261
|
+
# Read the task first: the trace recorders read it before writing, so on a
|
|
262
|
+
# backend that locks per key read this orders a purge against them.
|
|
263
|
+
@store.get(SCOPE, key_for(id))
|
|
264
|
+
@store.list(ModelMetricsStore::SCOPE, ModelMetricsStore.task_prefix(id)).each do |key|
|
|
265
|
+
@store.delete(ModelMetricsStore::SCOPE, key)
|
|
266
|
+
end
|
|
267
|
+
@store.delete(LLMTraceStore::SCOPE, id.to_s)
|
|
268
|
+
@store.delete(ModelMetricsStore::TURN_SCOPE, id.to_s)
|
|
269
|
+
@store.delete(SCOPE, key_for(id))
|
|
270
|
+
end
|
|
205
271
|
end
|
|
206
272
|
|
|
207
273
|
private
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require_relative "../env_schema"
|
|
4
5
|
|
|
5
6
|
module Insika
|
|
6
7
|
module Telemetry
|
|
@@ -22,9 +23,9 @@ module Insika
|
|
|
22
23
|
# nothing is emitted and the dashboard shows a gap instead of a lie.
|
|
23
24
|
#
|
|
24
25
|
# Token accounting (matches `Executor#usage_of`, which mirrors the providers):
|
|
25
|
-
#
|
|
26
|
-
# - `cached_input` given -> cached tokens are billed at that rate
|
|
27
|
-
#
|
|
26
|
+
# All buckets are disjoint, matching RubyLLM::Tokens.
|
|
27
|
+
# - `cached_input` given -> cached tokens are billed at that rate;
|
|
28
|
+
# absent -> they use the input rate.
|
|
28
29
|
# - `cache_write` given -> cache-creation tokens billed at that rate; absent
|
|
29
30
|
# -> at the input rate (they are input tokens the provider did write).
|
|
30
31
|
class Pricing
|
|
@@ -47,21 +48,22 @@ module Insika
|
|
|
47
48
|
|
|
48
49
|
rate = rate_for(usage[:model]) or return nil
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
buckets = [[usage[:input_tokens], rate["input"]],
|
|
52
|
+
[usage[:output_tokens], rate["output"]],
|
|
53
|
+
[usage[:cached_tokens], rate["cached_input"] || rate["input"]],
|
|
54
|
+
[usage[:cache_creation_tokens], rate["cache_write"] || rate["input"]]]
|
|
55
|
+
return nil if buckets.all? { |count, _| count.nil? }
|
|
56
|
+
return nil if buckets.any? { |count, price| count.to_i.positive? && price.nil? }
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
fresh = cached_rate ? [input - cached, 0].max : input
|
|
57
|
-
|
|
58
|
-
millionths = fresh * rate["input"].to_f +
|
|
59
|
-
(cached_rate ? cached * cached_rate.to_f : 0.0) +
|
|
60
|
-
written * (rate["cache_write"] || rate["input"]).to_f +
|
|
61
|
-
output * rate["output"].to_f
|
|
58
|
+
millionths = buckets.sum { |count, price| count.to_i * price.to_f }
|
|
62
59
|
(millionths / 1_000_000.0).round(PRECISION)
|
|
63
60
|
end
|
|
64
61
|
|
|
62
|
+
# The operator's table from INSIKA_MODEL_PRICING (empty when unset): the ONE
|
|
63
|
+
# cost rule Studio, traces and metrics share — the cost the provider library
|
|
64
|
+
# reported, and these rates only for what it could not price.
|
|
65
|
+
def self.from_env(env = ENV) = parse(Insika::EnvSchema.read("INSIKA_MODEL_PRICING", env))
|
|
66
|
+
|
|
65
67
|
# Parses the operator's table. Accepts a JSON object; anything else (blank,
|
|
66
68
|
# malformed, not an object) -> an EMPTY Pricing, never an exception: telemetry
|
|
67
69
|
# config must not be able to stop a boot.
|
|
@@ -18,8 +18,8 @@ module Insika
|
|
|
18
18
|
# a deliberate LOW-CARDINALITY subset of the span attributes — never task_id or
|
|
19
19
|
# session_id. `meter:` nil -> spans only (the metrics SDK is optional).
|
|
20
20
|
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
21
|
+
# Native attempt-time cost is the default. An operator-declared rates table
|
|
22
|
+
# can override it with a negotiated-price estimate (see Pricing).
|
|
23
23
|
#
|
|
24
24
|
# PURE/testable: talks to a DUCK-TYPED `tracer` (start_span/set_attribute/
|
|
25
25
|
# record_error/finish) and `meter` (create_counter/create_histogram -> add/
|
|
@@ -29,7 +29,7 @@ module Insika
|
|
|
29
29
|
# Robust: `record` NEVER raises (telemetry doesn't bring down a turn). Timestamps
|
|
30
30
|
# come from each event's `meta.at` (spans reconstructed with real time).
|
|
31
31
|
class Recorder
|
|
32
|
-
Turn = Struct.new(:span, :tools, :labels, :start) # tools = FIFO queue of open tools
|
|
32
|
+
Turn = Struct.new(:span, :tools, :labels, :start, :requests, :requests_truncated) # tools = FIFO queue of open tools
|
|
33
33
|
OpenTool = Struct.new(:span, :name, :start)
|
|
34
34
|
|
|
35
35
|
# Ceiling of open turns: a kill -9 without a terminal event would leave the turn
|
|
@@ -41,9 +41,11 @@ module Insika
|
|
|
41
41
|
# renaming one breaks every dashboard built on it.
|
|
42
42
|
class Instruments
|
|
43
43
|
attr_reader :turns, :turn_duration, :tokens, :cost, :tool_calls, :tool_duration,
|
|
44
|
-
:cache_hit_rate, :loop_intervened, :context_compacted
|
|
44
|
+
:cache_hit_rate, :loop_intervened, :context_compacted, :tool_blocked, :confirmations, :llm_attempts, :llm_failures
|
|
45
45
|
|
|
46
46
|
def initialize(meter)
|
|
47
|
+
@llm_attempts = meter.create_counter("insika.llm.attempts", unit: "{attempt}", description: "Native model attempts")
|
|
48
|
+
@llm_failures = meter.create_counter("insika.llm.failures", unit: "{attempt}", description: "Failed native model attempts")
|
|
47
49
|
@turns = meter.create_counter("insika.turns", unit: "{turn}",
|
|
48
50
|
description: "Turns completed, by outcome")
|
|
49
51
|
@turn_duration = meter.create_histogram("insika.turn.duration", unit: "s",
|
|
@@ -60,6 +62,10 @@ module Insika
|
|
|
60
62
|
description: "Prompt-cache hit rate of a turn (cached / billed prompt tokens)")
|
|
61
63
|
@loop_intervened = meter.create_counter("insika.tool.loop_intervened", unit: "{intervention}",
|
|
62
64
|
description: "Loop-detector warnings delivered to the model")
|
|
65
|
+
@tool_blocked = meter.create_counter("insika.tool.blocked", unit: "{call}",
|
|
66
|
+
description: "Tool calls blocked before execution")
|
|
67
|
+
@confirmations = meter.create_counter("insika.confirmation", unit: "{hold}",
|
|
68
|
+
description: "Calls held for the customer's confirmation, by outcome")
|
|
63
69
|
@context_compacted = meter.create_counter("insika.context.compacted", unit: "{compaction}",
|
|
64
70
|
description: "In-session compactions persisted (RFC-0044)")
|
|
65
71
|
end
|
|
@@ -77,10 +83,16 @@ module Insika
|
|
|
77
83
|
data = event.data || {}
|
|
78
84
|
case event.type
|
|
79
85
|
when :task_started then start_turn(meta, data)
|
|
86
|
+
when :llm_request, :llm_usage then model_event(meta, data, event.type)
|
|
80
87
|
when :tool_call then start_tool(meta, data)
|
|
81
88
|
when :tool_result then finish_tool(meta)
|
|
82
89
|
when :data_tool_call then point_tool(meta, data)
|
|
83
90
|
when :tool_loop_intervened then count_loop(meta, data)
|
|
91
|
+
when :tool_blocked then count_blocked(meta, data)
|
|
92
|
+
when :confirmation_requested then count_confirmation(meta, data, "requested")
|
|
93
|
+
when :confirmation_confirmed then count_confirmation(meta, data, "confirmed")
|
|
94
|
+
when :confirmation_cancelled then count_confirmation(meta, data, "cancelled")
|
|
95
|
+
when :confirmation_expired then count_confirmation(meta, data, "expired")
|
|
84
96
|
when :context_compacted then count_compaction(data)
|
|
85
97
|
when :task_completed then finish_turn(meta, data, :ok)
|
|
86
98
|
when :task_failed then finish_turn(meta, data, :error)
|
|
@@ -105,7 +117,7 @@ module Insika
|
|
|
105
117
|
"insika.turn", parent: nil, start_time: at,
|
|
106
118
|
attributes: labels.merge(attrs("insika.task_id" => id, "insika.session_id" => meta[:session_id]))
|
|
107
119
|
)
|
|
108
|
-
@turns[id] = Turn.new(span, [], labels, at)
|
|
120
|
+
@turns[id] = Turn.new(span, [], labels, at, [], false)
|
|
109
121
|
end
|
|
110
122
|
|
|
111
123
|
def start_tool(meta, data)
|
|
@@ -144,11 +156,80 @@ module Insika
|
|
|
144
156
|
at = ts(meta[:at])
|
|
145
157
|
usage = data[:usage]
|
|
146
158
|
set_usage(turn.span, usage)
|
|
159
|
+
rerank_cost = rerank_cost(turn)
|
|
160
|
+
if rerank_cost
|
|
161
|
+
turn.span.set_attribute("insika.cost.rerank_usd", rerank_cost)
|
|
162
|
+
chat_cost = estimated_cost(usage)
|
|
163
|
+
turn.span.set_attribute("insika.cost.total_usd", chat_cost + rerank_cost) if chat_cost
|
|
164
|
+
end
|
|
147
165
|
turn.span.set_attribute("insika.status", status.to_s)
|
|
148
|
-
turn.span.record_error(data[:
|
|
166
|
+
turn.span.record_error(exception_class(data[:error])) if status == :error
|
|
167
|
+
finish_requests(turn)
|
|
149
168
|
turn.tools.each { |t| t.span.finish(end_time: at) } # orphans (failure mid-way)
|
|
150
169
|
turn.span.finish(end_time: at)
|
|
151
|
-
count_turn(turn, usage, status.to_s, elapsed(turn.start, at))
|
|
170
|
+
count_turn(turn, usage, status.to_s, elapsed(turn.start, at), rerank_cost)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def rerank_cost(turn)
|
|
174
|
+
return nil if turn.requests_truncated
|
|
175
|
+
|
|
176
|
+
requests = turn.requests.select { |entry| entry["type"] == "llm_request" && entry["operation"] == "rerank" }
|
|
177
|
+
attempts = turn.requests.select { |entry| entry["type"] == "llm_usage" && entry["operation"] == "rerank" }
|
|
178
|
+
return nil if attempts.empty? || attempts.any? { |entry| !entry["cost"].is_a?(Numeric) }
|
|
179
|
+
return nil if requests.any? { |request| attempts.none? { |entry| entry["request_id"] == request["request_id"] } }
|
|
180
|
+
|
|
181
|
+
attempts.sum { |entry| entry["cost"] }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def exception_class(value)
|
|
185
|
+
value.is_a?(String) && value.match?(/\A[A-Z]\w*(?:::[A-Z]\w*)*\z/) ? value[0, 256] : "Task failed"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def model_event(meta, data, type)
|
|
189
|
+
turn = @turns[meta[:task_id]] or return
|
|
190
|
+
entry = LLMTraceStore.sanitize(data.merge("type" => type.to_s, "at" => meta[:at]))
|
|
191
|
+
turn.requests << entry
|
|
192
|
+
if turn.requests.size > LLMTraceStore::MAX_PER_TASK
|
|
193
|
+
turn.requests.shift
|
|
194
|
+
turn.requests_truncated = true
|
|
195
|
+
end
|
|
196
|
+
return unless @instruments && type == :llm_usage
|
|
197
|
+
|
|
198
|
+
labels = turn.labels.merge(attrs("insika.model" => entry["model"], "insika.provider" => entry["provider"],
|
|
199
|
+
"insika.operation" => entry["operation"], "insika.status" => entry["status"]))
|
|
200
|
+
@instruments.llm_attempts.add(1, attributes: labels)
|
|
201
|
+
@instruments.llm_failures.add(1, attributes: labels) if entry["status"] == "failed"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Success usage can arrive after request completion. Reconstruct at the turn
|
|
205
|
+
# terminal, so exported spans include late usage without modifying closed spans.
|
|
206
|
+
def finish_requests(turn)
|
|
207
|
+
turn.span.set_attribute("insika.llm.truncated", true) if turn.requests_truncated
|
|
208
|
+
turn.requests.group_by { |entry| entry["request_id"] }.each do |id, entries|
|
|
209
|
+
next if id.nil? || id.empty?
|
|
210
|
+
request = entries.find { |entry| entry["type"] == "llm_request" }
|
|
211
|
+
next unless request && request["duration_ms"]
|
|
212
|
+
|
|
213
|
+
at = ts(request["at"])
|
|
214
|
+
next unless at
|
|
215
|
+
attempts = entries.select { |entry| entry["type"] == "llm_usage" }
|
|
216
|
+
attributes = attrs("insika.llm.request_id" => id, "insika.model" => request["model"],
|
|
217
|
+
"insika.provider" => request["provider"], "insika.operation" => request["operation"],
|
|
218
|
+
"insika.status" => request["status"], "insika.llm.duration_ms" => request["duration_ms"],
|
|
219
|
+
"insika.llm.truncated" => turn.requests_truncated,
|
|
220
|
+
"insika.llm.attempts" => attempts.size, "insika.llm.failures" => attempts.count { |e| e["status"] == "failed" })
|
|
221
|
+
attempts.each_with_index do |attempt, index|
|
|
222
|
+
(%w[status] + LLMTraceStore::NUMBER_FIELDS - %w[turn duration_ms]).each do |key|
|
|
223
|
+
attributes["insika.llm.attempt.#{index + 1}.#{key}"] = attempt[key] unless attempt[key].nil?
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
span = @tracer.start_span("insika.llm.request", parent: turn.span,
|
|
227
|
+
start_time: at - request["duration_ms"] / 1000.0, attributes: attributes)
|
|
228
|
+
span.record_error(exception_class(request["exception_class"])) if request["status"] == "failed"
|
|
229
|
+
span.finish(end_time: at)
|
|
230
|
+
rescue StandardError
|
|
231
|
+
# A failed exporter must not prevent remaining requests or the turn closing.
|
|
232
|
+
end
|
|
152
233
|
end
|
|
153
234
|
|
|
154
235
|
def set_usage(span, usage)
|
|
@@ -161,13 +242,14 @@ module Insika
|
|
|
161
242
|
span.set_attribute("insika.tokens.cache_creation", usage[:cache_creation_tokens]) if usage[:cache_creation_tokens]
|
|
162
243
|
span.set_attribute("insika.model", usage[:model].to_s) if usage[:model]
|
|
163
244
|
span.set_attribute("insika.model_source", usage[:model_source].to_s) if usage[:model_source]
|
|
245
|
+
span.set_attribute("insika.provider", usage[:provider].to_s) if usage[:provider]
|
|
164
246
|
cost = estimated_cost(usage)
|
|
165
247
|
span.set_attribute("insika.cost.usd", cost) if cost
|
|
166
248
|
end
|
|
167
249
|
|
|
168
250
|
# --- metrics (no-op when no meter was injected) ------------------------
|
|
169
251
|
|
|
170
|
-
def count_turn(turn, usage, status, seconds)
|
|
252
|
+
def count_turn(turn, usage, status, seconds, rerank_cost = nil)
|
|
171
253
|
return unless @instruments
|
|
172
254
|
|
|
173
255
|
model = usage && usage[:model]
|
|
@@ -175,6 +257,7 @@ module Insika
|
|
|
175
257
|
@instruments.turns.add(1, attributes: labels)
|
|
176
258
|
@instruments.turn_duration.record(seconds, attributes: labels) if seconds
|
|
177
259
|
count_usage(turn, usage)
|
|
260
|
+
@instruments.cost.add(rerank_cost, attributes: turn.labels.merge("insika.operation" => "rerank")) if rerank_cost
|
|
178
261
|
count_cache_hit(turn, usage)
|
|
179
262
|
end
|
|
180
263
|
|
|
@@ -206,6 +289,26 @@ module Insika
|
|
|
206
289
|
@instruments.loop_intervened.add(1, attributes: labels)
|
|
207
290
|
end
|
|
208
291
|
|
|
292
|
+
# One counter for the whole life of a customer hold; `outcome` tells the
|
|
293
|
+
# four moments apart, so requested minus the other three is what is open.
|
|
294
|
+
def count_confirmation(meta, data, outcome)
|
|
295
|
+
return unless @instruments
|
|
296
|
+
|
|
297
|
+
turn = @turns[meta[:task_id]]
|
|
298
|
+
labels = (turn ? turn.labels : {}).merge(attrs("insika.tool" => data[:tool]&.to_s,
|
|
299
|
+
"insika.outcome" => outcome))
|
|
300
|
+
@instruments.confirmations.add(1, attributes: labels)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def count_blocked(meta, data)
|
|
304
|
+
return unless @instruments
|
|
305
|
+
|
|
306
|
+
turn = @turns[meta[:task_id]] or return
|
|
307
|
+
labels = turn.labels.merge(attrs("insika.tool" => data[:name]&.to_s,
|
|
308
|
+
"insika.gate" => data[:gate]&.to_s))
|
|
309
|
+
@instruments.tool_blocked.add(1, attributes: labels)
|
|
310
|
+
end
|
|
311
|
+
|
|
209
312
|
# A compaction persisted (`:context_compacted`, RFC-0044) — counted by
|
|
210
313
|
# agent/model, INDEPENDENT of any open turn: it fires post-turn, usually
|
|
211
314
|
# after task_completed already closed the span.
|
|
@@ -238,7 +341,9 @@ module Insika
|
|
|
238
341
|
@instruments.tool_duration.record(seconds, attributes: labels) if seconds
|
|
239
342
|
end
|
|
240
343
|
|
|
241
|
-
|
|
344
|
+
# Turn usage arrives already priced per response (Executor#usage_of applies
|
|
345
|
+
# the same rule), so its cost_usd wins; the table covers usage that was not.
|
|
346
|
+
def estimated_cost(usage) = usage&.fetch(:cost_usd, nil) || @pricing&.cost(usage)
|
|
242
347
|
|
|
243
348
|
# -----------------------------------------------------------------------
|
|
244
349
|
|
|
@@ -248,6 +353,7 @@ module Insika
|
|
|
248
353
|
|
|
249
354
|
turn.tools.each { |t| t.span.finish(end_time: nil) }
|
|
250
355
|
turn.span.set_attribute("insika.status", "abandoned")
|
|
356
|
+
finish_requests(turn)
|
|
251
357
|
turn.span.finish(end_time: nil)
|
|
252
358
|
count_turn(turn, nil, "abandoned", nil)
|
|
253
359
|
end
|