insika 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +69 -3
- data/README.md +1 -1
- data/bin/insika +22 -7
- data/docs/AGENTS.md +129 -5
- data/docs/CHANNELS.md +1 -1
- data/docs/CONTEXT.md +22 -5
- data/docs/DEPLOY.md +30 -10
- data/docs/EMBEDDING.md +11 -7
- data/docs/EVALS.md +1 -1
- data/docs/LOADTEST.md +3 -2
- data/docs/OBSERVABILITY.md +11 -2
- data/docs/REFINEMENT.md +6 -6
- data/docs/RELEASING.md +7 -7
- data/docs/RUNNING-LOCAL.md +1 -1
- data/docs/SECURITY.md +24 -11
- data/docs/SKILLS.md +189 -3
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/index.md +1 -1
- data/docs/onboarding/start.md +1 -1
- data/lib/insika/agent_profile.rb +89 -22
- data/lib/insika/alert_dispatcher.rb +139 -0
- data/lib/insika/baseline_store.rb +2 -2
- data/lib/insika/budget_ledger.rb +135 -0
- data/lib/insika/channel_delivery.rb +14 -11
- data/lib/insika/channel_registry.rb +1 -1
- data/lib/insika/channels/relay.rb +3 -3
- data/lib/insika/channels/web/widget.js +2 -2
- data/lib/insika/channels/web.rb +7 -7
- data/lib/insika/channels/webhook.rb +58 -0
- data/lib/insika/chat_builder.rb +62 -13
- data/lib/insika/circuit_state.rb +114 -0
- data/lib/insika/coercion.rb +8 -0
- data/lib/insika/commands/agent_payload.rb +5 -3
- data/lib/insika/commands/create_agent.rb +2 -2
- data/lib/insika/commands/create_session.rb +1 -1
- data/lib/insika/commands/delete_llm_provider.rb +1 -1
- data/lib/insika/commands/delete_skill.rb +43 -0
- data/lib/insika/commands/gate_refinement.rb +12 -12
- data/lib/insika/commands/import_mcp_tools.rb +1 -1
- data/lib/insika/commands/import_tools.rb +4 -4
- data/lib/insika/commands/issue_tenant_token.rb +41 -0
- data/lib/insika/commands/resolve_refinement.rb +1 -1
- data/lib/insika/commands/revoke_token.rb +39 -0
- data/lib/insika/commands/rotate_tenant_token.rb +43 -0
- data/lib/insika/commands/run_refinement.rb +5 -5
- data/lib/insika/commands/send_message.rb +9 -9
- data/lib/insika/commands/set_agent_tools.rb +1 -1
- data/lib/insika/commands/set_skill_agents.rb +60 -19
- data/lib/insika/commands/trigger_workflow.rb +1 -1
- data/lib/insika/commands/update_agent.rb +1 -1
- data/lib/insika/commands/write_data_tool.rb +1 -1
- data/lib/insika/commands/write_golden.rb +1 -1
- data/lib/insika/commands/write_skill.rb +19 -9
- data/lib/insika/config_store.rb +8 -4
- data/lib/insika/context/builder.rb +2 -2
- data/lib/insika/context/fragment.rb +27 -3
- data/lib/insika/context/priority.rb +3 -2
- data/lib/insika/context/providers/memory.rb +1 -1
- data/lib/insika/context/providers/request.rb +1 -1
- data/lib/insika/context/providers/session.rb +17 -2
- data/lib/insika/context/providers/skill.rb +5 -1
- data/lib/insika/context/providers/skill_trigger.rb +128 -0
- data/lib/insika/context_trace_store.rb +92 -0
- data/lib/insika/delegation_store.rb +2 -2
- data/lib/insika/doctor.rb +250 -5
- data/lib/insika/dsl/runtime.rb +12 -9
- data/lib/insika/dsl/server_boot.rb +4 -3
- data/lib/insika/dsl/system.rb +1 -1
- data/lib/insika/dsl.rb +72 -15
- data/lib/insika/edge_limiter.rb +144 -6
- data/lib/insika/egress_guard.rb +3 -3
- data/lib/insika/env_schema.rb +13 -10
- data/lib/insika/errors.rb +61 -5
- data/lib/insika/evals/assertions.rb +12 -12
- data/lib/insika/evals/baseline.rb +3 -3
- data/lib/insika/evals/golden.rb +8 -8
- data/lib/insika/evals/judge.rb +7 -7
- data/lib/insika/evals/pairwise.rb +3 -3
- data/lib/insika/evals/report.rb +2 -2
- data/lib/insika/evals/runner.rb +6 -6
- data/lib/insika/evals/transport.rb +2 -2
- data/lib/insika/event_stream.rb +23 -5
- data/lib/insika/executor.rb +423 -108
- data/lib/insika/frontmatter.rb +1 -1
- data/lib/insika/golden_store.rb +2 -2
- data/lib/insika/http_client.rb +3 -3
- data/lib/insika/inbound_log.rb +1 -1
- data/lib/insika/llm_configurator.rb +3 -3
- data/lib/insika/loop_detector.rb +143 -0
- data/lib/insika/mcp_http_client.rb +4 -4
- data/lib/insika/mcp_tool_ingestor.rb +6 -6
- data/lib/insika/message_origin.rb +2 -2
- data/lib/insika/model_resolver.rb +1 -1
- data/lib/insika/model_selection.rb +5 -4
- data/lib/insika/onboarding.rb +2 -2
- data/lib/insika/outbox_store.rb +2 -2
- data/lib/insika/overlay_tool_registry.rb +3 -4
- data/lib/insika/pack.rb +3 -3
- data/lib/insika/pack_importer.rb +17 -15
- data/lib/insika/pending_action_store.rb +1 -1
- data/lib/insika/plugin/loader.rb +2 -2
- data/lib/insika/policy/policy.rb +1 -1
- data/lib/insika/profile_source.rb +12 -6
- data/lib/insika/provider_error_classifier.rb +160 -0
- data/lib/insika/queue_policy.rb +2 -2
- data/lib/insika/recovery.rb +47 -6
- data/lib/insika/refinement/candidate.rb +4 -4
- data/lib/insika/refinement/evidence_collector.rb +6 -6
- data/lib/insika/refinement/gate.rb +7 -7
- data/lib/insika/refinement/panel.rb +7 -7
- data/lib/insika/refinement/proposer.rb +9 -9
- data/lib/insika/refinement_store.rb +12 -12
- data/lib/insika/reliability.rb +185 -0
- data/lib/insika/safety/config.rb +2 -2
- data/lib/insika/safety/detectors.rb +5 -5
- data/lib/insika/safety/factory.rb +3 -3
- data/lib/insika/safety/input_guardrail.rb +19 -4
- data/lib/insika/safety/moderator.rb +19 -11
- data/lib/insika/safety/output_filter.rb +2 -2
- data/lib/insika/safety/output_validator.rb +2 -2
- data/lib/insika/safety/safe_responses.rb +1 -1
- data/lib/insika/sandbox/boundary.rb +2 -2
- data/lib/insika/sandbox.rb +1 -1
- data/lib/insika/server/app.rb +223 -51
- data/lib/insika/server/boot.rb +4 -4
- data/lib/insika/server/rack_app.rb +15 -7
- data/lib/insika/server/responses.rb +18 -8
- data/lib/insika/server/tenant_auth.rb +61 -0
- data/lib/insika/session_actor.rb +3 -3
- data/lib/insika/session_store.rb +1 -1
- data/lib/insika/settings_store.rb +5 -5
- data/lib/insika/shutdown.rb +4 -4
- data/lib/insika/skill_catalog.rb +127 -20
- data/lib/insika/skill_store.rb +70 -22
- data/lib/insika/steer_injector.rb +1 -1
- data/lib/insika/store.rb +1 -1
- data/lib/insika/studio/app.rb +183 -61
- data/lib/insika/studio/assets/dist/application.js +25 -24
- data/lib/insika/studio/forms.rb +13 -18
- data/lib/insika/studio/nav_icons.rb +1 -1
- data/lib/insika/studio/views/_message.erb +2 -2
- data/lib/insika/studio/views/agent_detail.erb +2 -2
- data/lib/insika/studio/views/agents.erb +1 -1
- data/lib/insika/studio/views/refinement.erb +4 -4
- data/lib/insika/studio/views/session.erb +78 -3
- data/lib/insika/studio/views/settings.erb +7 -12
- data/lib/insika/studio/views/skills.erb +67 -12
- data/lib/insika/subagent_graph.rb +3 -3
- data/lib/insika/task_actor.rb +3 -3
- data/lib/insika/task_store.rb +1 -1
- data/lib/insika/telemetry/pricing.rb +3 -3
- data/lib/insika/telemetry/recorder.rb +1 -1
- data/lib/insika/telemetry.rb +2 -2
- data/lib/insika/testing/store_contract.rb +27 -27
- data/lib/insika/tick.rb +122 -0
- data/lib/insika/token_store.rb +168 -0
- data/lib/insika/tool_assembly.rb +5 -5
- data/lib/insika/tool_definition.rb +8 -8
- data/lib/insika/tool_envelope.rb +1 -1
- data/lib/insika/tool_manifest.rb +6 -6
- data/lib/insika/tool_output_compressor.rb +100 -0
- data/lib/insika/tool_store.rb +1 -1
- data/lib/insika/tool_trace_store.rb +1 -1
- data/lib/insika/tools/concurrency.rb +2 -2
- data/lib/insika/tools/data_defined_tool.rb +4 -5
- data/lib/insika/tools/load_skill.rb +61 -3
- data/lib/insika/tools/stuck_signal.rb +44 -0
- data/lib/insika/tools/subagent.rb +4 -4
- data/lib/insika/tools/subagents.rb +1 -1
- data/lib/insika/turn_output.rb +2 -2
- data/lib/insika/turn_state.rb +17 -13
- data/lib/insika/turn_timing.rb +2 -2
- data/lib/insika/usage_ledger.rb +1 -1
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +77 -26
- data/lib/insika/workflow.rb +1 -1
- data/lib/insika/workflow_registry.rb +1 -1
- data/lib/insika.rb +32 -15
- metadata +19 -2
- data/lib/insika/server/admin_auth.rb +0 -29
data/lib/insika/studio/forms.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Studio
|
|
4
|
-
# Form → command-payload parsing for the Studio App
|
|
4
|
+
# Form → command-payload parsing for the Studio App. A mixin, NOT a
|
|
5
5
|
# standalone object: `include`d into App so every method keeps `self` == the
|
|
6
6
|
# App instance and reaches its request helpers (`presence`, `split_list`) and
|
|
7
7
|
# per-request state (`@agent`) exactly as before. Pure extraction — the bodies
|
|
@@ -15,7 +15,7 @@ module Studio
|
|
|
15
15
|
v = presence(r.params[field])
|
|
16
16
|
limits[field.to_sym] = Integer(v) if v
|
|
17
17
|
end
|
|
18
|
-
# Per-agent edge overrides
|
|
18
|
+
# Per-agent edge overrides. Unlike the timeouts (always present via
|
|
19
19
|
# DEFAULT_LIMITS), these are OPT-IN keys: blank DELETES the override (inherit
|
|
20
20
|
# the platform edge settings); 0 explicitly disables for this agent.
|
|
21
21
|
%w[chat_rate_limit agent_token_ceiling].each do |field|
|
|
@@ -38,7 +38,7 @@ module Studio
|
|
|
38
38
|
}
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
# Guardrails config from the form
|
|
41
|
+
# Guardrails config from the form. The config form OWNS these fields,
|
|
42
42
|
# so the patch reflects the whole guardrails state. String values round-trip
|
|
43
43
|
# cleanly through the JSON store; Safety::Config normalizes on read. A blank
|
|
44
44
|
# moderator drops the key (deterministic only).
|
|
@@ -54,7 +54,7 @@ module Studio
|
|
|
54
54
|
out
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
# Per-category safe-reply overrides (
|
|
57
|
+
# Per-category safe-reply overrides (config over convention). Only
|
|
58
58
|
# the non-blank fields are persisted; Safety::Config normalizes on read.
|
|
59
59
|
def guardrail_responses_patch(r)
|
|
60
60
|
%w[default injection sexual abuse escalate].each_with_object({}) do |cat, acc|
|
|
@@ -62,7 +62,7 @@ module Studio
|
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
# Per-agent generation params
|
|
65
|
+
# Per-agent generation params. The config form OWNS these fields, so
|
|
66
66
|
# the patch reflects the whole form state: a blank field drops the key (and an
|
|
67
67
|
# all-blank form clears params to {}). temperature/max_tokens MUST be Numeric —
|
|
68
68
|
# ModelSelection#apply_params guards on `numeric?` and silently skips a String —
|
|
@@ -79,7 +79,7 @@ module Studio
|
|
|
79
79
|
out
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
-
# Per-agent model fence
|
|
82
|
+
# Per-agent model fence: { "allow" => [refs] } where a ref is
|
|
83
83
|
# "provider/model", "provider/*" or "model". Blank textarea -> nil (NO fence,
|
|
84
84
|
# all models — parity). Enforced on the RESOLVED model by the ModelResolver,
|
|
85
85
|
# so a per-chat pin can never escape it.
|
|
@@ -101,7 +101,7 @@ module Studio
|
|
|
101
101
|
|
|
102
102
|
# Fields a tool CAN carry that this form does not render. The store REPLACES the
|
|
103
103
|
# record on write, so anything missing here is erased — a save that only fixed a
|
|
104
|
-
# typo in the description would silently drop them (the class of bug
|
|
104
|
+
# typo in the description would silently drop them (the class of bug already
|
|
105
105
|
# paid for once). `stored` carries them through untouched.
|
|
106
106
|
UNEDITED_TOOL_FIELDS = %w[group tags halt_when].freeze
|
|
107
107
|
|
|
@@ -166,22 +166,17 @@ module Studio
|
|
|
166
166
|
|
|
167
167
|
def settings_patch(r)
|
|
168
168
|
patch = {
|
|
169
|
-
"streaming" => r.params["streaming"] == "1"
|
|
170
|
-
"compaction" => {
|
|
171
|
-
"enabled" => r.params["compaction_enabled"] == "1"
|
|
172
|
-
}
|
|
169
|
+
"streaming" => r.params["streaming"] == "1"
|
|
173
170
|
}
|
|
174
171
|
{ "request_timeout" => "request_timeout", "max_retries" => "max_retries",
|
|
175
172
|
"turn_timeout" => "turn_timeout", "tool_timeout" => "tool_timeout" }.each_key do |f|
|
|
176
173
|
v = presence(r.params[f])
|
|
177
174
|
patch[f] = Integer(v) if v
|
|
178
175
|
end
|
|
179
|
-
kl = presence(r.params["keep_last"])
|
|
180
|
-
patch["compaction"]["keep_last"] = Integer(kl) if kl
|
|
181
176
|
patch
|
|
182
177
|
end
|
|
183
178
|
|
|
184
|
-
# Edge limits
|
|
179
|
+
# Edge limits — the platform rate-limit/cost layer, saved
|
|
185
180
|
# from its OWN form (a sub-resource, like models). The limit fields write nil
|
|
186
181
|
# when blank (off — the EdgeLimiter reads nil/0 as off); the windows fall back
|
|
187
182
|
# to the built-in defaults when cleared (the limiter guards non-positive).
|
|
@@ -209,7 +204,7 @@ module Studio
|
|
|
209
204
|
{ case: parsed }
|
|
210
205
|
end
|
|
211
206
|
|
|
212
|
-
# Evals graders
|
|
207
|
+
# Evals graders. `judges` is one `provider/model` per LINE — a
|
|
213
208
|
# textarea, because the SIZE of the panel is the point and a fixed pair of fields
|
|
214
209
|
# would cap it at two. A bare `model` (no slash) is valid: the provider is then
|
|
215
210
|
# inferred by RubyLLM, same as everywhere else.
|
|
@@ -258,7 +253,7 @@ module Studio
|
|
|
258
253
|
raise Insika::ValidationError, "#{field} must be an integer (got #{raw.inspect})"
|
|
259
254
|
end
|
|
260
255
|
|
|
261
|
-
# LLM config v2
|
|
256
|
+
# LLM config v2 — the platform model layer, saved from its OWN form (a
|
|
262
257
|
# sub-resource, like providers) so the general-settings save never touches it and
|
|
263
258
|
# vice-versa. The scalar refs are always present (blank -> nil, which the
|
|
264
259
|
# deep-merge writes and the ModelResolver reads as "no platform default");
|
|
@@ -270,12 +265,12 @@ module Studio
|
|
|
270
265
|
"default_provider" => presence(r.params["default_provider"]),
|
|
271
266
|
"utility_model" => presence(r.params["utility_model"]),
|
|
272
267
|
"fallback_models" => split_list(r.params["fallback_models"]),
|
|
273
|
-
# GLOBAL reasoning default (
|
|
268
|
+
# GLOBAL reasoning default (4-layer). Blank -> nil = provider default.
|
|
274
269
|
"thinking" => presence(r.params["thinking"])
|
|
275
270
|
}
|
|
276
271
|
end
|
|
277
272
|
|
|
278
|
-
# Per-model reasoning defaults (
|
|
273
|
+
# Per-model reasoning defaults (the per-model layer). One line per model:
|
|
279
274
|
# <provider/model | model> | <off|on|low|medium|high>
|
|
280
275
|
# CSP forbids JS for dynamic rows, so a textarea of lines is the honest path
|
|
281
276
|
# (same idiom as the MCP env / tool params). A blank effort -> {} for that ref
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Studio
|
|
4
|
-
# Nav SVG icons for the Studio App
|
|
4
|
+
# Nav SVG icons for the Studio App. A mixin included into App: the
|
|
5
5
|
# `nav_icon` view helper and its icon table, moved verbatim out of app.rb.
|
|
6
6
|
# Inline SVG is CSP-safe (an HTML element, not an external resource).
|
|
7
7
|
module NavIcons
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%# Shared transcript bubble/tool-card
|
|
1
|
+
<%# Shared transcript bubble/tool-card — one visual language for the
|
|
2
2
|
playground history, the optimistic echo and the read-only session viewer.
|
|
3
3
|
Locals: m = stored message hash {"role","content","at"}; prev = the message before
|
|
4
4
|
it in the thread, or nil (used only to spot a steered message). `content` may be a
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
when it is NOT the natural producer for the role — otherwise every bubble
|
|
23
23
|
would carry a label that says nothing. %>
|
|
24
24
|
<% origin = Insika::MessageOrigin.origin_of(m) unless Insika::MessageOrigin.customer?(m) || Insika::MessageOrigin.agent?(m) %>
|
|
25
|
-
<%# STEERED
|
|
25
|
+
<%# STEERED: a customer message sitting right after a tool result arrived
|
|
26
26
|
while the turn was running and was appended at a tool-batch boundary. Derived
|
|
27
27
|
from position, not from a field — a steered message declares no origin, and it
|
|
28
28
|
is right not to: a person typed it. Nothing else in the engine ever puts a user
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
</label>
|
|
70
70
|
|
|
71
71
|
<fieldset class="subgroup">
|
|
72
|
-
<legend>guardrails <span class="muted">— content safety
|
|
72
|
+
<legend>guardrails <span class="muted">— content safety</span></legend>
|
|
73
73
|
<label class="check">
|
|
74
74
|
<input type="checkbox" name="guardrail_input" value="1"<%== " checked" if guardrail_field(@guardrails, :input, true) %>>
|
|
75
75
|
input guardrail <span class="muted">— block injection / abuse / inappropriate before the LLM</span>
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
</select>
|
|
89
89
|
</label>
|
|
90
90
|
<label>LLM moderator <span class="muted">(model ref, or <code>on</code> for the platform utility_model; blank = deterministic only)</span>
|
|
91
|
-
<input type="text" name="guardrail_moderator" value="<%= guardrail_field(@guardrails, :moderator, "") %>" placeholder="deepseek/deepseek-
|
|
91
|
+
<input type="text" name="guardrail_moderator" value="<%= guardrail_field(@guardrails, :moderator, "") %>" placeholder="deepseek/deepseek-v4-flash">
|
|
92
92
|
</label>
|
|
93
93
|
</div>
|
|
94
94
|
<details class="newfile">
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
<input type="text" name="id" placeholder="bia" required>
|
|
49
49
|
</label>
|
|
50
50
|
<label>model
|
|
51
|
-
<input type="text" name="model" placeholder="deepseek-
|
|
51
|
+
<input type="text" name="model" placeholder="deepseek-v4-flash" required>
|
|
52
52
|
</label>
|
|
53
53
|
</div>
|
|
54
54
|
<label>provider <span class="muted">(optional)</span>
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
</div>
|
|
40
40
|
<% end %>
|
|
41
41
|
|
|
42
|
-
<%#
|
|
42
|
+
<%# The review IS the product. A gated proposal shows the exact
|
|
43
43
|
edit, what it claims to fix, and how it scored against the accepted baseline,
|
|
44
44
|
because approving is a decision and a decision needs those three things. %>
|
|
45
45
|
<% if @proposal %>
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
<span class="mono"><%= @proposal.gate["passed_cases"] %>/<%= @proposal.gate["cases"] %> case(s)</span>
|
|
52
52
|
<span class="muted">no regression against <%= @proposal.gate["baseline_cases"] %> baselined case(s)</span>
|
|
53
53
|
<span class="muted">· proposed by <span class="mono"><%= cand["proposer"] %></span></span>
|
|
54
|
-
<%#
|
|
54
|
+
<%# When a PANEL ran, the operator is looking at the winner of
|
|
55
55
|
several, and which ones it beat is part of the decision. %>
|
|
56
56
|
<% if @proposal.panel.size > 1 %>
|
|
57
57
|
<span class="muted">· best of <%= @proposal.panel.size %> candidate(s)</span>
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
<% else %>
|
|
126
126
|
<h2>Findings<span class="sub"><%= @run.findings.size %> · <%= @run.status %><% if @run.excluded.to_i.positive? %> · <%= @run.excluded %> turn(s) excluded<% end %> · <%= @run.finished_at || @run.started_at %></span></h2>
|
|
127
127
|
|
|
128
|
-
<%#
|
|
128
|
+
<%# The model writes the candidate, the gate scores it. Offered on
|
|
129
129
|
a finished report and only when nothing is already awaiting an answer: a second
|
|
130
130
|
proposal while one is pending would ask the operator to review two edits to the
|
|
131
131
|
same file that were never scored together. %>
|
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
</div>
|
|
148
148
|
<% end %>
|
|
149
149
|
|
|
150
|
-
<%#
|
|
150
|
+
<%# `mode: auto_apply` changed this agent's files with nobody in
|
|
151
151
|
the loop, so the page owes the operator exactly three things: what changed, why,
|
|
152
152
|
and where to undo it. The write versioned, so History IS the undo. %>
|
|
153
153
|
<% if @run.status == :applied && (@run.decision || {})["by"] == "auto_apply" %>
|
|
@@ -12,6 +12,57 @@
|
|
|
12
12
|
<a class="btn" href="/studio/playground?session_id=<%= Rack::Utils.escape(@session.id) %>">Continue in playground →</a>
|
|
13
13
|
</header>
|
|
14
14
|
|
|
15
|
+
<% ctx_traces = @context_traces || [] %>
|
|
16
|
+
<% unless ctx_traces.empty? %>
|
|
17
|
+
<div class="card tooltrace">
|
|
18
|
+
<div class="trace-head">
|
|
19
|
+
<h2>Context</h2>
|
|
20
|
+
<span class="muted">tokens by category per turn (estimated) — no content stored</span>
|
|
21
|
+
</div>
|
|
22
|
+
<% ctx_traces.each do |c| %>
|
|
23
|
+
<div class="trace-turn">
|
|
24
|
+
<div class="turn-label">turn <%= c["turn"] %> · <%= c["at"] %></div>
|
|
25
|
+
<%# Skills injected by context (triggers/skills_eager) leave NO tool call in
|
|
26
|
+
the transcript, so this card is the only record that they were active.
|
|
27
|
+
The names live in the body, but the count belongs in the SUMMARY: the
|
|
28
|
+
card is collapsed, and "was the skill on?" is the question an operator
|
|
29
|
+
asks on every single turn. %>
|
|
30
|
+
<% skills = Array((c["categories"] || {}).dig("skilltrigger", "labels")).uniq %>
|
|
31
|
+
<details class="trace-item ok">
|
|
32
|
+
<summary>
|
|
33
|
+
<span class="badge"><%= c["used"] %> / <%= c["cap"] %></span>
|
|
34
|
+
<strong>context</strong>
|
|
35
|
+
<% unless skills.empty? %>
|
|
36
|
+
<span class="badge">skills <%= skills.length %></span>
|
|
37
|
+
<% end %>
|
|
38
|
+
<% unless Array(c["evicted"]).empty? %>
|
|
39
|
+
<span class="lat">evicted: <%= Array(c["evicted"]).join(", ") %></span>
|
|
40
|
+
<% end %>
|
|
41
|
+
</summary>
|
|
42
|
+
<% (c["categories"] || {}).sort_by { |_k, v| -(v["tokens"] || 0) }.each do |name, cat| %>
|
|
43
|
+
<div class="kv"><span><%= name %></span>
|
|
44
|
+
<div><%= cat["tokens"] %> tokens · <%= cat["fragments"] %> fragment(s)<%== " · pinned" if (cat["pinned"] || 0).positive? %>
|
|
45
|
+
<%# WHICH skills the turn injected AND WHY — the only after-the-fact %>
|
|
46
|
+
<%# record of activation once the bodies arrive by context instead of a %>
|
|
47
|
+
<%# tool call. The reason is the half the operator was missing: a name %>
|
|
48
|
+
<%# alone does not say whether they triggered it or the agent always has it. %>
|
|
49
|
+
<% labels = Array(cat["labels"]).uniq %>
|
|
50
|
+
<% unless labels.empty? %>
|
|
51
|
+
<br><span class="lat"><%= labels.map { |l| skill_label(l) }.join(", ") %></span>
|
|
52
|
+
<% end %>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<% end %>
|
|
56
|
+
<% tools = c["tools"] || {} %>
|
|
57
|
+
<div class="kv"><span>tools</span>
|
|
58
|
+
<div><%= tools["tokens"] %> tokens · <%= tools["count"] %> tool schema(s)</div>
|
|
59
|
+
</div>
|
|
60
|
+
</details>
|
|
61
|
+
</div>
|
|
62
|
+
<% end %>
|
|
63
|
+
</div>
|
|
64
|
+
<% end %>
|
|
65
|
+
|
|
15
66
|
<% traces = @tool_traces || {} %>
|
|
16
67
|
<% unless traces.empty? %>
|
|
17
68
|
<div class="card tooltrace">
|
|
@@ -54,9 +105,33 @@
|
|
|
54
105
|
<div class="empty">This session has no messages.</div>
|
|
55
106
|
<% else %>
|
|
56
107
|
<div class="thread">
|
|
57
|
-
<%#
|
|
58
|
-
|
|
59
|
-
|
|
108
|
+
<%# A skill loaded by the model shows up here on its own: load_skill is a tool,
|
|
109
|
+
so its body is a persisted `role: tool` message. A skill injected by CONTEXT
|
|
110
|
+
(triggers/skills_eager) is part of the system prompt — never a message — so
|
|
111
|
+
the thread had no trace of it at all. Synthesized from the context trace so
|
|
112
|
+
both activation paths read the same way in the transcript.
|
|
113
|
+
|
|
114
|
+
Placed by TIME, not by counting turns: every message of a turn is persisted
|
|
115
|
+
together at the end of it, and the context stage always runs before that, so
|
|
116
|
+
each entry lands at the top of its own turn. Ordinal counting would drift on
|
|
117
|
+
a steered message (one that joins a turn already running). A message with no
|
|
118
|
+
`at` (older sessions) simply flushes nothing. %>
|
|
119
|
+
<% pending = (@context_traces || [])
|
|
120
|
+
.select { |c| Array((c["categories"] || {}).dig("skilltrigger", "labels")).any? }
|
|
121
|
+
.sort_by { |c| c["at"].to_s } %>
|
|
122
|
+
<% Array(@session.messages).each_with_index do |m, i| %>
|
|
123
|
+
<% while (entry = pending.first) && !m["at"].to_s.empty? && entry["at"].to_s <= m["at"].to_s %>
|
|
124
|
+
<% pending.shift %>
|
|
125
|
+
<% labels = Array(entry["categories"]["skilltrigger"]["labels"]).uniq %>
|
|
126
|
+
<details class="toolcard skill">
|
|
127
|
+
<summary><span class="arrow">↑</span><strong>skills · <%= skill_mode(labels) %> (<%= labels.length %>)</strong><span class="at"><%= entry["at"] %></span></summary>
|
|
128
|
+
<pre><%= labels.map { |l| skill_label(l) }.join("\n") %></pre>
|
|
129
|
+
</details>
|
|
130
|
+
<% end %>
|
|
131
|
+
<%# `prev` is what makes a STEERED message visible: a customer message that lands
|
|
132
|
+
right after a tool result arrived mid-run. See _message.erb. %>
|
|
133
|
+
<%== render("_message", locals: { m: m, prev: i.positive? ? @session.messages[i - 1] : nil }) %>
|
|
134
|
+
<% end %>
|
|
60
135
|
</div>
|
|
61
136
|
<% end %>
|
|
62
137
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<%# Settings — drill-down (T2): section list | selected section's form(s).
|
|
2
2
|
@section (general|models|llm) comes from ?s= and drives the detail. %>
|
|
3
3
|
<% sections = [
|
|
4
|
-
["general", "General", "streaming, timeouts
|
|
4
|
+
["general", "General", "streaming, timeouts"],
|
|
5
5
|
["models", "Model defaults", "platform model layer (Chat › Agent › platform)"],
|
|
6
6
|
["edge", "Edge limits", "rate-limit per chat, token ceiling per agent"],
|
|
7
7
|
["evals", "Evals", "who grades a rubric, and how a panel agrees"],
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
<button type="submit" form="set-general" class="btn primary" data-turbo-submits-with="Saving…">Save</button>
|
|
28
28
|
</div>
|
|
29
29
|
<div class="drill-pane-body">
|
|
30
|
-
<p class="muted">Streaming
|
|
30
|
+
<p class="muted">Streaming and timeouts — saved via <code>update_settings</code> (durable, applies next turn).</p>
|
|
31
31
|
<form id="set-general" method="post" action="/studio/settings">
|
|
32
32
|
<%== csrf_tag %>
|
|
33
33
|
<label class="check">
|
|
@@ -40,11 +40,6 @@
|
|
|
40
40
|
<label>turn_timeout (s)<input type="text" name="turn_timeout" value="<%= @settings["turn_timeout"] %>" inputmode="numeric"></label>
|
|
41
41
|
<label>tool_timeout (s)<input type="text" name="tool_timeout" value="<%= @settings["tool_timeout"] %>" inputmode="numeric"></label>
|
|
42
42
|
</div>
|
|
43
|
-
<label class="check">
|
|
44
|
-
<input type="checkbox" name="compaction_enabled" value="1"<%== " checked" if @settings.dig("compaction", "enabled") %>>
|
|
45
|
-
context compaction
|
|
46
|
-
</label>
|
|
47
|
-
<label>compaction.keep_last (messages)<input type="text" name="keep_last" value="<%= @settings.dig("compaction", "keep_last") %>" inputmode="numeric"></label>
|
|
48
43
|
</form>
|
|
49
44
|
</div>
|
|
50
45
|
|
|
@@ -58,11 +53,11 @@
|
|
|
58
53
|
<form id="set-models" method="post" action="/studio/settings/models">
|
|
59
54
|
<%== csrf_tag %>
|
|
60
55
|
<div class="grid-2">
|
|
61
|
-
<label>default_model <span class="muted">(agents without their own model)</span><input type="text" name="default_model" value="<%= @settings["default_model"] %>" placeholder="deepseek-
|
|
56
|
+
<label>default_model <span class="muted">(agents without their own model)</span><input type="text" name="default_model" value="<%= @settings["default_model"] %>" placeholder="deepseek-v4-flash"></label>
|
|
62
57
|
<label>default_provider <span class="muted">(optional)</span><input type="text" name="default_provider" value="<%= @settings["default_provider"] %>" placeholder="deepseek"></label>
|
|
63
58
|
</div>
|
|
64
|
-
<label>fallback_models <span class="muted">(ordered chain; <code>provider/model</code> or <code>model</code>, comma or newline)</span><input type="text" name="fallback_models" value="<%= Array(@settings["fallback_models"]).join(", ") %>" placeholder="deepseek/deepseek-
|
|
65
|
-
<label>utility_model <span class="muted">(cheap internal tasks — titles, distillation)</span><input type="text" name="utility_model" value="<%= @settings["utility_model"] %>" placeholder="deepseek-
|
|
59
|
+
<label>fallback_models <span class="muted">(ordered chain; <code>provider/model</code> or <code>model</code>, comma or newline)</span><input type="text" name="fallback_models" value="<%= Array(@settings["fallback_models"]).join(", ") %>" placeholder="deepseek/deepseek-v4-flash, openai/gpt-4o-mini"></label>
|
|
60
|
+
<label>utility_model <span class="muted">(cheap internal tasks — titles, distillation)</span><input type="text" name="utility_model" value="<%= @settings["utility_model"] %>" placeholder="deepseek-v4-flash"></label>
|
|
66
61
|
<label>reasoning <span class="muted">(global default — off/on/effort; blank = provider default)</span>
|
|
67
62
|
<%== thinking_select("thinking", @settings["thinking"], "provider default") %>
|
|
68
63
|
</label>
|
|
@@ -107,7 +102,7 @@
|
|
|
107
102
|
<p class="muted">Who grades a <a href="/studio/evals">case</a>'s rubric. <strong>One model per line</strong> as <code>provider/model</code> — several lines make a PANEL, and two different models disagreeing is the signal worth having (sampling one model N times mostly measures its own variance). Empty = deterministic assertions only, and rubric'd cases read as <code>judge_pending</code>.</p>
|
|
108
103
|
<form id="set-evals" method="post" action="/studio/settings/evals">
|
|
109
104
|
<%== csrf_tag %>
|
|
110
|
-
<label>judges<textarea name="judges" rows="4" spellcheck="false" placeholder="deepseek/deepseek-
|
|
105
|
+
<label>judges<textarea name="judges" rows="4" spellcheck="false" placeholder="deepseek/deepseek-v4-flash openai/gpt-5-mini"><%= Array(@settings.dig("evals", "judges")).map { |j| [j["provider"], j["model"]].compact.reject(&:empty?).join("/") }.join("\n") %></textarea></label>
|
|
111
106
|
<div class="grid-2">
|
|
112
107
|
<label>aggregate <span class="muted">(how the panel's scores combine)</span>
|
|
113
108
|
<select name="aggregate">
|
|
@@ -147,7 +142,7 @@
|
|
|
147
142
|
<label>auth_header<input type="text" name="auth_header" value="<%= p["auth_header"] %>" placeholder="Authorization"></label>
|
|
148
143
|
</div>
|
|
149
144
|
<label>api_key<input type="password" name="api_key" value="<%= p["api_key"] %>" autocomplete="off"></label>
|
|
150
|
-
<label>models <span class="muted">(comma or newline)</span><input type="text" name="models" value="<%= Array(p["models"]).join(", ") %>" placeholder="deepseek-
|
|
145
|
+
<label>models <span class="muted">(comma or newline)</span><input type="text" name="models" value="<%= Array(p["models"]).join(", ") %>" placeholder="deepseek-v4-flash, deepseek-reasoner"></label>
|
|
151
146
|
<div class="actions"><button type="submit" class="primary" data-turbo-submits-with="Saving…">Save provider</button></div>
|
|
152
147
|
</form>
|
|
153
148
|
</div>
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
<%# Skills — drill-down (T2): master list | detail editor + availability.
|
|
2
2
|
Every skill route renders THIS view (index / new / :name); @selected drives
|
|
3
|
-
which skill is open (nil = none, "" = new).
|
|
3
|
+
which skill is open (nil = none, "" = new). @scope_agent set = editing THAT
|
|
4
|
+
agent's own version of the skill, not the shared one. %>
|
|
5
|
+
<% scope = @scope_agent %>
|
|
6
|
+
<% form_action = scope ? "/studio/agents/#{Rack::Utils.escape(scope)}/skills/#{Rack::Utils.escape(@selected.to_s)}" : "/studio/skills" %>
|
|
4
7
|
<div class="drill" data-controller="list-filter">
|
|
5
8
|
<aside class="drill-pane drill-master">
|
|
6
9
|
<div class="drill-pane-head">
|
|
@@ -36,15 +39,30 @@
|
|
|
36
39
|
<% new = @selected == "" %>
|
|
37
40
|
<div class="drill-pane-head">
|
|
38
41
|
<div class="title">
|
|
39
|
-
<nav class="crumbs"><a href="/studio/skills">skills</a> / <span><%= new ? "new" : @selected %></span
|
|
40
|
-
<strong class="mono"><%= new ? "New skill" : @selected %></strong>
|
|
42
|
+
<nav class="crumbs"><a href="/studio/skills">skills</a> / <span><%= new ? "new" : @selected %></span><% if scope %> / <span>for <%= scope %></span><% end %></nav>
|
|
43
|
+
<strong class="mono"><%= new ? "New skill" : @selected %><% if scope %> <span class="pill warn">specialized for <%= scope %></span><% end %></strong>
|
|
41
44
|
</div>
|
|
42
45
|
<button type="submit" form="skill-form" class="btn primary" data-turbo-submits-with="Saving…">Save skill</button>
|
|
43
46
|
</div>
|
|
44
47
|
<div class="drill-pane-body">
|
|
45
|
-
|
|
48
|
+
<%# The specialization is one MORE record, not a fork: the name stays the bare
|
|
49
|
+
shared name everywhere the model and the operator see it, and deleting the
|
|
50
|
+
override falls back to the shared body. %>
|
|
51
|
+
<% if scope %>
|
|
52
|
+
<div class="drill-section">
|
|
53
|
+
<p class="muted">This is <strong><%= scope %></strong>'s own version of <code><%= @selected %></code>. Every
|
|
54
|
+
other agent keeps the <a href="/studio/skills/<%= Rack::Utils.escape(@selected.to_s) %>">shared skill</a>,
|
|
55
|
+
and the name does not change — the allowlist, <code><available_skills></code> and
|
|
56
|
+
<code>load_skill</code> all still say <code><%= @selected %></code>.</p>
|
|
57
|
+
<form method="post" action="/studio/agents/<%= Rack::Utils.escape(scope) %>/skills/<%= Rack::Utils.escape(@selected.to_s) %>/delete">
|
|
58
|
+
<%== csrf_tag %>
|
|
59
|
+
<div class="actions"><button type="submit" data-turbo-confirm="Remove this specialization? <%= scope %> falls back to the shared skill.">Stop specializing</button></div>
|
|
60
|
+
</form>
|
|
61
|
+
</div>
|
|
62
|
+
<% end %>
|
|
63
|
+
<form id="skill-form" method="post" action="<%= form_action %>" data-controller="dirty-guard">
|
|
46
64
|
<%== csrf_tag %>
|
|
47
|
-
<%
|
|
65
|
+
<% if !new && !scope %><input type="hidden" name="name" value="<%= @selected %>"><% end %>
|
|
48
66
|
<% if new %>
|
|
49
67
|
<label>name <span class="muted">(canonical — same as <code>name:</code> in the frontmatter)</span>
|
|
50
68
|
<input type="text" name="name" placeholder="my-skill" required>
|
|
@@ -56,29 +74,66 @@
|
|
|
56
74
|
</div>
|
|
57
75
|
</form>
|
|
58
76
|
|
|
59
|
-
<%
|
|
77
|
+
<% if !new && !scope %>
|
|
60
78
|
<div class="drill-section">
|
|
61
79
|
<div class="drill-section-head">
|
|
62
80
|
<h3>Availability</h3>
|
|
63
|
-
<span class="muted">
|
|
81
|
+
<span class="muted">per agent: <strong>allowed</strong> = can <code>load_skill</code> it; <strong>always on</strong> = the body
|
|
82
|
+
is in the prompt every turn. Both are the AGENT's decision, so one shared skill can be
|
|
83
|
+
always-on for one and discretionary for another. An "all" allowlist is left untouched.</span>
|
|
64
84
|
</div>
|
|
65
85
|
<form method="post" action="/studio/skills/<%= Rack::Utils.escape(@selected) %>/agents">
|
|
66
86
|
<%== csrf_tag %>
|
|
67
87
|
<% if @agents.empty? %>
|
|
68
88
|
<p class="muted">No agents configured.</p>
|
|
69
89
|
<% else %>
|
|
70
|
-
<div class="check-grid">
|
|
90
|
+
<div class="check-grid agent-grid">
|
|
71
91
|
<% @agents.each do |a| %>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
<
|
|
75
|
-
|
|
92
|
+
<% own = Array((@specialized || {})[@selected]).include?(a.id) %>
|
|
93
|
+
<div class="agent-check">
|
|
94
|
+
<div class="agent-check-name">
|
|
95
|
+
<span class="mono"><%= a.id %></span>
|
|
96
|
+
<% if own %>
|
|
97
|
+
<a class="pill warn" href="/studio/agents/<%= Rack::Utils.escape(a.id) %>/skills/<%= Rack::Utils.escape(@selected) %>">own version</a>
|
|
98
|
+
<% end %>
|
|
99
|
+
</div>
|
|
100
|
+
<label class="check tool-check">
|
|
101
|
+
<input type="checkbox" name="agent_ids[]" value="<%= a.id %>"<%== " checked" if skill_enabled_for?(a, @selected) %>>
|
|
102
|
+
<span class="mono">allowed</span>
|
|
103
|
+
</label>
|
|
104
|
+
<label class="check tool-check">
|
|
105
|
+
<input type="checkbox" name="eager_ids[]" value="<%= a.id %>"<%== " checked" if skill_eager_for?(a, @selected) %>>
|
|
106
|
+
<span class="mono">always on</span>
|
|
107
|
+
</label>
|
|
108
|
+
</div>
|
|
76
109
|
<% end %>
|
|
77
110
|
</div>
|
|
78
111
|
<div class="actions"><button type="submit" class="primary" data-turbo-submits-with="Applying…">Apply to agents</button></div>
|
|
79
112
|
<% end %>
|
|
80
113
|
</form>
|
|
81
114
|
</div>
|
|
115
|
+
|
|
116
|
+
<%# Specializing is what keeps a shared skill shared: without it the only way
|
|
117
|
+
to give one agent a different version was to fork it under a new name and
|
|
118
|
+
edit the allowlist, which throws the sharing away. %>
|
|
119
|
+
<% others = @agents.reject { |a| Array((@specialized || {})[@selected]).include?(a.id) } %>
|
|
120
|
+
<% if others.any? %>
|
|
121
|
+
<div class="drill-section">
|
|
122
|
+
<div class="drill-section-head">
|
|
123
|
+
<h3>Specialize</h3>
|
|
124
|
+
<span class="muted">give ONE agent its own version of this skill, seeded from the body above — same name, no fork</span>
|
|
125
|
+
</div>
|
|
126
|
+
<form method="post" action="/studio/skills/<%= Rack::Utils.escape(@selected) %>/specialize">
|
|
127
|
+
<%== csrf_tag %>
|
|
128
|
+
<label>agent
|
|
129
|
+
<select name="agent_id" required>
|
|
130
|
+
<% others.each do |a| %><option value="<%= a.id %>"><%= a.id %></option><% end %>
|
|
131
|
+
</select>
|
|
132
|
+
</label>
|
|
133
|
+
<div class="actions"><button type="submit" data-turbo-submits-with="Creating…">Specialize for this agent</button></div>
|
|
134
|
+
</form>
|
|
135
|
+
</div>
|
|
136
|
+
<% end %>
|
|
82
137
|
<% end %>
|
|
83
138
|
</div>
|
|
84
139
|
<% end %>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Insika
|
|
4
|
-
# Definition-time integrity of the subagent delegation graph (
|
|
5
|
-
#
|
|
4
|
+
# Definition-time integrity of the subagent delegation graph (
|
|
5
|
+
# A pure function over `{id => [child_ids]}`: detects CYCLES and
|
|
6
6
|
# computes the max delegation DEPTH, raising a typed SubagentError so the
|
|
7
7
|
# authoring Command (CreateAgent/UpdateAgent) fails cleanly and boot refuses a
|
|
8
8
|
# bad static set. Cycle + bounded depth are exactly Flue's definition-time
|
|
@@ -21,7 +21,7 @@ module Insika
|
|
|
21
21
|
|
|
22
22
|
# Max children a single `spawn_subagents` fan-out may run — also the concurrency
|
|
23
23
|
# bound (N concurrent LLM calls hit the provider rate limit + the per-agent token
|
|
24
|
-
# ceiling of
|
|
24
|
+
# ceiling of). Override with INSIKA_SUBAGENT_FANOUT_CAP.
|
|
25
25
|
DEFAULT_FANOUT_CAP = 8
|
|
26
26
|
|
|
27
27
|
module_function
|
data/lib/insika/task_actor.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Insika
|
|
|
9
9
|
# `await` as the cooperative SUSPENSION primitive. Cancellation/suspension only
|
|
10
10
|
# at stage boundaries — never in the middle of an operation.
|
|
11
11
|
class TaskActor
|
|
12
|
-
# `user_message` is posted by `Executor#steer_into_running`
|
|
12
|
+
# `user_message` is posted by `Executor#steer_into_running` and
|
|
13
13
|
# consumed by `SteerInjector` at a tool-batch boundary. `pause`/`resume` (operator),
|
|
14
14
|
# `approval` (human-in-the-loop), `timeout`/`heartbeat`
|
|
15
15
|
# (watchdog/liveness, observation).
|
|
@@ -61,14 +61,14 @@ module Insika
|
|
|
61
61
|
# the flag).
|
|
62
62
|
def pause_requested? = @pause_requested
|
|
63
63
|
|
|
64
|
-
#
|
|
64
|
+
# takes the steered messages and clears the buffer, WITHOUT
|
|
65
65
|
# observing anything else in the mailbox: whatever is not a `:user_message` is put
|
|
66
66
|
# back, in the order it arrived.
|
|
67
67
|
#
|
|
68
68
|
# Why not `drain!`: this runs INSIDE RubyLLM's tool loop (a `after_message`
|
|
69
69
|
# callback), and `drain!` raises on `:cancel`. Cancellation is only ever observed
|
|
70
70
|
# at the Executor's own stage boundaries — that is what keeps a tool batch one
|
|
71
|
-
# unit of work (
|
|
71
|
+
# unit of work (and for the same rule under `turn_timeout`).
|
|
72
72
|
# Injecting a message must not quietly become a new place a turn can die.
|
|
73
73
|
def take_user_messages!
|
|
74
74
|
@mailbox.size.times do
|
data/lib/insika/task_store.rb
CHANGED
|
@@ -131,7 +131,7 @@ module Insika
|
|
|
131
131
|
to_task(record)
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
-
#
|
|
134
|
+
# (`collect`): appends a fragment to a task's message while it is
|
|
135
135
|
# still waiting at the door. -> Task.
|
|
136
136
|
#
|
|
137
137
|
# ONLY on :queued, and that guard is the whole safety of the feature: once a
|
|
@@ -4,7 +4,7 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module Insika
|
|
6
6
|
module Telemetry
|
|
7
|
-
# Estimated cost of a turn, from a rates table given as DATA
|
|
7
|
+
# Estimated cost of a turn, from a rates table given as DATA.
|
|
8
8
|
#
|
|
9
9
|
# Insika never ships prices: they change weekly, differ per contract and per
|
|
10
10
|
# region, and a stale table in the engine would be worse than no number at all.
|
|
@@ -13,11 +13,11 @@ module Insika
|
|
|
13
13
|
#
|
|
14
14
|
# Rates are **USD per million tokens** (the unit every provider publishes):
|
|
15
15
|
#
|
|
16
|
-
# { "deepseek-
|
|
16
|
+
# { "deepseek-v4-flash" => { "input" => 0.27, "output" => 1.10,
|
|
17
17
|
# "cached_input" => 0.07, "cache_write" => 0.34 } }
|
|
18
18
|
#
|
|
19
19
|
# A key matches the model id the provider reports, with or without the
|
|
20
|
-
# `provider/` prefix (`deepseek/deepseek-
|
|
20
|
+
# `provider/` prefix (`deepseek/deepseek-v4-flash` and `deepseek-v4-flash` both hit the
|
|
21
21
|
# entry above). An UNKNOWN model -> nil: a missing price is not a zero cost, so
|
|
22
22
|
# nothing is emitted and the dashboard shows a gap instead of a lie.
|
|
23
23
|
#
|
|
@@ -12,7 +12,7 @@ module Insika
|
|
|
12
12
|
# insika.data_tool), correlated by task_id. Latency is the span duration; the
|
|
13
13
|
# agent/tenant/model/tokens/cost ride as ATTRIBUTES.
|
|
14
14
|
#
|
|
15
|
-
# METRICS
|
|
15
|
+
# METRICS: the SAME events also feed counters and histograms, so
|
|
16
16
|
# volume/latency/tokens/cost are chartable WITHOUT span aggregation (which not
|
|
17
17
|
# every backend does, and none does cheaply at retention). Metric attributes are
|
|
18
18
|
# a deliberate LOW-CARDINALITY subset of the span attributes — never task_id or
|
data/lib/insika/telemetry.rb
CHANGED
|
@@ -5,7 +5,7 @@ require_relative "telemetry/pricing"
|
|
|
5
5
|
require_relative "telemetry/recorder"
|
|
6
6
|
|
|
7
7
|
module Insika
|
|
8
|
-
# OPT-IN observability
|
|
8
|
+
# OPT-IN observability: OTEL mounted at the edge, core untouched. Rides
|
|
9
9
|
# the Event Stream — the Recorder consumes the events and emits spans and metrics.
|
|
10
10
|
# Off (the default) -> `setup` returns nil and nothing is loaded or instrumented
|
|
11
11
|
# (parity, zero overhead). The OTEL gems are only REQUIRED lazily in `setup`
|
|
@@ -17,7 +17,7 @@ module Insika
|
|
|
17
17
|
# OTEL_* keys are the OpenTelemetry SDK's own env and stay verbatim; only our opt-in
|
|
18
18
|
# toggle is renamed (INSIKA_OTEL, with the HARNESS_OTEL alias still read).
|
|
19
19
|
#
|
|
20
|
-
# Metrics
|
|
20
|
+
# Metrics ride the same switch and the SAME standard env: the SDK
|
|
21
21
|
# registers a periodic metric reader when the OTLP metrics exporter is loadable,
|
|
22
22
|
# and `OTEL_METRICS_EXPORTER=none` turns metrics off while traces stay on. No
|
|
23
23
|
# Insika-specific toggle is invented for it.
|