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/evals/golden.rb
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
|
|
5
|
-
# Evals — the quality harness
|
|
6
|
-
# call it (the refinement gate of
|
|
5
|
+
# Evals — the quality harness. It lives in `lib/` so the engine itself can
|
|
6
|
+
# call it (the refinement gate of needs to score a candidate agent, and a
|
|
7
7
|
# second copy of the judge would be the worst possible outcome), but it stays a
|
|
8
8
|
# CLIENT: it reaches a running deployment over HTTP through `HttpTransport` and never
|
|
9
9
|
# reads a store directly. `evals/run.rb` is a thin CLI over this module.
|
|
@@ -28,17 +28,17 @@ module Insika
|
|
|
28
28
|
# Names of the negative assertions to run (e.g. "pii_leak", "tool_error").
|
|
29
29
|
def must_not = Array(expect["must_not"]).map(&:to_s)
|
|
30
30
|
|
|
31
|
-
# How much the agent should ask before acting
|
|
31
|
+
# How much the agent should ask before acting. nil = the store
|
|
32
32
|
# has no opinion and only the rubric decides.
|
|
33
33
|
def policy = GoldenLoader.presence(expect["policy"])
|
|
34
34
|
|
|
35
|
-
# What the DEPLOYMENT must have for this case to mean anything
|
|
35
|
+
# What the DEPLOYMENT must have for this case to mean anything.
|
|
36
36
|
# Empty = runs everywhere.
|
|
37
37
|
def required_tools = Array(requires["tools"]).map(&:to_s)
|
|
38
38
|
def required_capabilities = Array(requires["capabilities"]).map(&:to_s)
|
|
39
39
|
def requirements? = !(required_tools + required_capabilities).empty?
|
|
40
40
|
|
|
41
|
-
# THE INCUMBENT'S CONVERSATION for the same opening
|
|
41
|
+
# THE INCUMBENT'S CONVERSATION for the same opening — the other
|
|
42
42
|
# half of a pairwise comparison. Data in the case, not a store read: the eval is
|
|
43
43
|
# a client, and a pair that lives in one reviewable file cannot go stale against
|
|
44
44
|
# a database nobody looked at.
|
|
@@ -47,14 +47,14 @@ module Insika
|
|
|
47
47
|
def reference? = !reference_messages.empty?
|
|
48
48
|
|
|
49
49
|
# Did a PERSON type part of the reference half? After a handoff the operator's
|
|
50
|
-
# words are stored as `role: assistant
|
|
50
|
+
# words are stored as `role: assistant`, and comparing a model to a human
|
|
51
51
|
# and calling it a win is a lie in both directions — so the pair is LABELLED and
|
|
52
52
|
# the report never prints the outcome without it.
|
|
53
53
|
def human_assisted?
|
|
54
54
|
reference_messages.any? { |m| MessageOrigin.origin_of(m) == MessageOrigin::OPERATOR }
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
# LLM-judge rubric + threshold (consumed in
|
|
57
|
+
# LLM-judge rubric + threshold (consumed in — deferred here).
|
|
58
58
|
def rubric = expect["rubric"]
|
|
59
59
|
def min_score = expect["min_score"]
|
|
60
60
|
end
|
|
@@ -126,7 +126,7 @@ module Insika
|
|
|
126
126
|
raise InvalidGolden, "#{where} needs a 'role' of user or assistant" unless %w[user assistant].include?(role)
|
|
127
127
|
|
|
128
128
|
text = presence(raw["text"]) || (raise InvalidGolden, "#{where} needs a non-empty 'text'")
|
|
129
|
-
# The SAME closed vocabulary the engine stamps
|
|
129
|
+
# The SAME closed vocabulary the engine stamps. A typo'd marker would
|
|
130
130
|
# read as "absent" downstream, which is how a human turn gets scored as the
|
|
131
131
|
# incumbent's model.
|
|
132
132
|
origin = begin
|
data/lib/insika/evals/judge.rb
CHANGED
|
@@ -4,7 +4,7 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module Insika
|
|
6
6
|
module Evals
|
|
7
|
-
# The LLM-judge
|
|
7
|
+
# The LLM-judge. Scores a golden's `rubric` against the
|
|
8
8
|
# actual assistant reply — the subjective layer on top of the deterministic
|
|
9
9
|
# asserts. Pure over an injected `ask` callable (prompt -> raw model text), so it's
|
|
10
10
|
# unit-testable without an LLM; the real ask (RubyLLM on the utility_model, temp 0)
|
|
@@ -13,7 +13,7 @@ module Insika
|
|
|
13
13
|
# Conservative by construction: an unparseable judge reply scores 0 (fails) rather
|
|
14
14
|
# than silently passing.
|
|
15
15
|
#
|
|
16
|
-
# A PANEL, not a single voice
|
|
16
|
+
# A PANEL, not a single voice. `quorum: N` samples ONE model N
|
|
17
17
|
# times, which measures that model's variance and little else — at temperature 0 it
|
|
18
18
|
# mostly returns the same answer, including the same blind spot. Two DIFFERENT
|
|
19
19
|
# models disagreeing about a rubric is the signal worth having, so `asks:` takes one
|
|
@@ -95,7 +95,7 @@ module Insika
|
|
|
95
95
|
end
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
# The `policy` is the one thing a rubric cannot carry alone
|
|
98
|
+
# The `policy` is the one thing a rubric cannot carry alone: how
|
|
99
99
|
# much this store wants the agent to ask before acting is a per-store decision,
|
|
100
100
|
# and a judge that is not TOLD it will guess — half the time wrongly. The
|
|
101
101
|
# deterministic half is `Assertions.policy_checks`; this is the other half.
|
|
@@ -149,11 +149,11 @@ module Insika
|
|
|
149
149
|
end
|
|
150
150
|
end
|
|
151
151
|
|
|
152
|
-
# Builds the configured judge panel from `settings["evals"]
|
|
152
|
+
# Builds the configured judge panel from `settings["evals"]`.
|
|
153
153
|
#
|
|
154
154
|
# It lives HERE and not in `evals/run.rb` because the CLI is no longer the only
|
|
155
155
|
# caller: the refinement gate scores a candidate with the SAME judges the operator
|
|
156
|
-
# configured, and
|
|
156
|
+
# configured, and is explicit that a second copy of the judge would be the
|
|
157
157
|
# worst possible outcome — the gate would then be grading against a rubric nobody
|
|
158
158
|
# tuned. One builder, two callers.
|
|
159
159
|
module JudgePanel
|
|
@@ -164,7 +164,7 @@ module Insika
|
|
|
164
164
|
# -> [Judge, [model names]] | nil when nobody is configured to ask. NIL AND NOT
|
|
165
165
|
# a no-op judge: a rubric'd case with no judge reads as `judge_pending`, which is
|
|
166
166
|
# visible, where a judge that always passes would be silent.
|
|
167
|
-
# `llm
|
|
167
|
+
# `llm`: the graph's own RubyLLM context; nil = the global
|
|
168
168
|
# constant. Only the deployment root and the CLI build judges today, and both
|
|
169
169
|
# are one graph per process — the seam keeps the default honest for the day
|
|
170
170
|
# an embedded graph gates a candidate on its own credentials.
|
|
@@ -187,7 +187,7 @@ module Insika
|
|
|
187
187
|
# Sugar for the callers that only want the judge (the gate).
|
|
188
188
|
def judge(settings, **kw) = build(settings, **kw)&.first
|
|
189
189
|
|
|
190
|
-
# The SAME configured models, asked a different question
|
|
190
|
+
# The SAME configured models, asked a different question. One
|
|
191
191
|
# builder because "who judges here" is one operator decision: a pairwise panel
|
|
192
192
|
# configured apart from the rubric panel would let a run be graded by judges
|
|
193
193
|
# nobody chose. -> [Pairwise, [model names]] | nil when nobody is configured.
|
|
@@ -4,7 +4,7 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module Insika
|
|
6
6
|
module Evals
|
|
7
|
-
# PAIRWISE AGAINST THE INCUMBENT
|
|
7
|
+
# PAIRWISE AGAINST THE INCUMBENT — the number that answers "can we
|
|
8
8
|
# replace it". An absolute 0.72 says a reply cleared a bar we invented; it says
|
|
9
9
|
# nothing about whether the system already answering 403,231 chats would have done
|
|
10
10
|
# better with the same customer.
|
|
@@ -29,7 +29,7 @@ module Insika
|
|
|
29
29
|
# 3. **A split panel stays split.** Averaging "better" and "worse" into
|
|
30
30
|
# "comparable" invents agreement that nobody expressed.
|
|
31
31
|
#
|
|
32
|
-
# Cost: 2 provider calls per judge per case
|
|
32
|
+
# Cost: 2 provider calls per judge per case. This is why it never runs as
|
|
33
33
|
# part of the gate and is opt-in on the CLI.
|
|
34
34
|
class Pairwise
|
|
35
35
|
BETTER = "better"
|
|
@@ -39,7 +39,7 @@ module Insika
|
|
|
39
39
|
UNKNOWN = "unknown"
|
|
40
40
|
|
|
41
41
|
# `vs` names WHO the reference half actually is: `agent` (model against model)
|
|
42
|
-
# or `human-assisted` (a person typed part of it,
|
|
42
|
+
# or `human-assisted` (a person typed part of it,'s `origin: operator`).
|
|
43
43
|
# It rides on the verdict rather than beside it so a report cannot print the
|
|
44
44
|
# outcome without the label.
|
|
45
45
|
Verdict = Struct.new(:outcome, :reason, :vs, :judges, :order_dependent, keyword_init: true) do
|
data/lib/insika/evals/report.rb
CHANGED
|
@@ -5,7 +5,7 @@ require "json"
|
|
|
5
5
|
module Insika
|
|
6
6
|
module Evals
|
|
7
7
|
# Renders a run's [CaseResult] as a machine-readable JSON blob (for the baseline
|
|
8
|
-
#
|
|
8
|
+
# gating in) and a human-readable markdown summary.
|
|
9
9
|
# Pure over the results — takes a clock value in, never reads it (so callers stay
|
|
10
10
|
# deterministic/testable).
|
|
11
11
|
module Report
|
|
@@ -90,7 +90,7 @@ module Insika
|
|
|
90
90
|
lines = ["# Eval report — #{at}", "",
|
|
91
91
|
"**#{h['passed']}/#{h['total'] - h['skipped']} passed** · #{h['failed']} failed" \
|
|
92
92
|
"#{" · #{h['skipped']} skipped" if h['skipped'].positive?}" \
|
|
93
|
-
"#{" · #{h['judge_pending']} awaiting judge
|
|
93
|
+
"#{" · #{h['judge_pending']} awaiting judge" if h['judge_pending'].positive?}", ""]
|
|
94
94
|
results.each do |r|
|
|
95
95
|
if r.skipped?
|
|
96
96
|
# WITH the reason, always: "12 skipped" alone is indistinguishable from a
|
data/lib/insika/evals/runner.rb
CHANGED
|
@@ -17,7 +17,7 @@ module Insika
|
|
|
17
17
|
# `tokens` is what the whole case cost, summed over its turns, or nil when no
|
|
18
18
|
# turn reported usage. `cached` is how much of that was served from the prompt
|
|
19
19
|
# cache, carried separately because it is the number that explains a total.
|
|
20
|
-
# Only the refinement gate reads them (
|
|
20
|
+
# Only the refinement gate reads them (records a run's cost); the
|
|
21
21
|
# report and the exit code are untouched.
|
|
22
22
|
RunCase = Struct.new(:result, :timings, :tokens, :cached, keyword_init: true)
|
|
23
23
|
|
|
@@ -26,13 +26,13 @@ module Insika
|
|
|
26
26
|
#
|
|
27
27
|
# capabilities: what the DEPLOYMENT has, per agent — anything answering
|
|
28
28
|
# `#for(agent_id)` with { "tools" =>, "capabilities" => } or nil. Used to skip a
|
|
29
|
-
# case the deployment cannot satisfy
|
|
29
|
+
# case the deployment cannot satisfy, BEFORE spending a turn on
|
|
30
30
|
# it. nil (or an unknown agent) = no resolution, and then a case with `requires`
|
|
31
31
|
# RUNS and says so in the report: "could not rule it out" is not a reason to
|
|
32
32
|
# stop testing something, and a suite that shrinks in silence is the failure
|
|
33
33
|
# this feature exists to avoid.
|
|
34
34
|
#
|
|
35
|
-
# pairwise: an Evals::Pairwise (optional
|
|
35
|
+
# pairwise: an Evals::Pairwise (optional). Only cases carrying a
|
|
36
36
|
# `reference:` are compared, and the verdict never touches pass/fail — it is the
|
|
37
37
|
# answer to "can we replace it", reported beside the suite's own verdict.
|
|
38
38
|
def initialize(transport:, judge: nil, conv_map: {}, capabilities: nil, pairwise: nil)
|
|
@@ -53,8 +53,8 @@ module Insika
|
|
|
53
53
|
skip = skip_reason(golden)
|
|
54
54
|
return RunCase.new(result: Assertions.skip(golden, skip), timings: []) if skip
|
|
55
55
|
|
|
56
|
-
# A backend that resolves state from a pre-existing conversation (e.g.
|
|
57
|
-
#
|
|
56
|
+
# A backend that resolves state from a pre-existing conversation (e.g. a
|
|
57
|
+
# consumer needing a real Chat UUID as X-Chat-Id) supplies it via conv_map; otherwise the
|
|
58
58
|
# synthetic "eval-<id>" keeps the adapter's own multi-turn continuation.
|
|
59
59
|
conv = @conv_map[golden.id] || "eval-#{golden.id}"
|
|
60
60
|
turns = []
|
|
@@ -76,7 +76,7 @@ module Insika
|
|
|
76
76
|
# Subjective layer: only when a judge is configured, the case has a rubric, and
|
|
77
77
|
# the turn ran cleanly (nothing to judge on an errored turn).
|
|
78
78
|
result.judge = @judge.score(golden: golden, result: last) if @judge && result.rubric && result.error.nil?
|
|
79
|
-
# Against the incumbent
|
|
79
|
+
# Against the incumbent. Same rule as the judge: nothing to
|
|
80
80
|
# compare on a turn that errored — half a conversation would lose the
|
|
81
81
|
# comparison for a reason that has nothing to do with the agent.
|
|
82
82
|
result.pairwise = @pairwise.compare(golden: golden, turns: turns) if @pairwise && result.error.nil?
|
|
@@ -16,7 +16,7 @@ module Insika
|
|
|
16
16
|
#
|
|
17
17
|
# `usage` is the turn's token counts as the deployment reported them
|
|
18
18
|
# (`response.completed`), or nil when the provider sent none. It is carried, never
|
|
19
|
-
# asserted on: the consumer is
|
|
19
|
+
# asserted on: the consumer is's refinement budget, which has to bound the
|
|
20
20
|
# cost of a gate replay and cannot invent the number. nil is preserved as nil
|
|
21
21
|
# rather than zeroed — "the provider did not say" and "it cost nothing" are
|
|
22
22
|
# different facts, and a budget that confuses them stops being a budget.
|
|
@@ -72,7 +72,7 @@ module Insika
|
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
# What the deployment HAS, per agent
|
|
75
|
+
# What the deployment HAS, per agent, read over the same gated
|
|
76
76
|
# `/v1` the replay uses. The eval stays a client: it asks the engine instead of
|
|
77
77
|
# keeping its own idea of which tools exist.
|
|
78
78
|
#
|
data/lib/insika/event_stream.rb
CHANGED
|
@@ -20,9 +20,11 @@ module Insika
|
|
|
20
20
|
# local :error event — the turn never waits on transport.
|
|
21
21
|
MAX_QUEUED = 1000
|
|
22
22
|
|
|
23
|
-
def initialize(task_id: nil, session_id: nil, on_close: nil)
|
|
23
|
+
def initialize(task_id: nil, session_id: nil, tenant: nil, types: nil, on_close: nil)
|
|
24
24
|
@task_id = task_id
|
|
25
25
|
@session_id = session_id
|
|
26
|
+
@tenant = tenant
|
|
27
|
+
@types = types
|
|
26
28
|
@on_close = on_close
|
|
27
29
|
@queue = Async::Queue.new
|
|
28
30
|
end
|
|
@@ -39,9 +41,21 @@ module Insika
|
|
|
39
41
|
|
|
40
42
|
# Meta filter: nil = matches any value. Events with no task_id in
|
|
41
43
|
# meta (e.g. :session_created) reach only subscribers with no task filter.
|
|
44
|
+
#
|
|
45
|
+
# A TENANT-scoped subscription is FAIL-CLOSED on the meta's tenant (WS1):
|
|
46
|
+
# an event that does not carry the tenant (control events, ignored turns)
|
|
47
|
+
# matches NO tenant subscription. The tenant only ever sees its own.
|
|
48
|
+
#
|
|
49
|
+
# `types:` (nil = any) keeps a subscriber's queue to the events it answers
|
|
50
|
+
# — an alert consumer must not sit behind a full-traffic stream's 1000-cap
|
|
51
|
+
# (WS6), and a filtered queue is the cheapest way to keep it there.
|
|
42
52
|
def matches?(event)
|
|
43
53
|
meta = event.meta || {}
|
|
44
|
-
|
|
54
|
+
owned = @tenant.nil? || meta[:tenant] == @tenant
|
|
55
|
+
|
|
56
|
+
owned &&
|
|
57
|
+
(@types.nil? || @types.include?(event.type)) &&
|
|
58
|
+
(@task_id.nil? || meta[:task_id] == @task_id) &&
|
|
45
59
|
(@session_id.nil? || meta[:session_id] == @session_id)
|
|
46
60
|
end
|
|
47
61
|
|
|
@@ -103,9 +117,13 @@ module Insika
|
|
|
103
117
|
end
|
|
104
118
|
|
|
105
119
|
# nil/nil = all events. Returns the Subscription (the caller iterates with
|
|
106
|
-
# `#each` on its own fiber).
|
|
107
|
-
|
|
108
|
-
|
|
120
|
+
# `#each` on its own fiber). `tenant:` scopes the stream to one tenant's
|
|
121
|
+
# events (WS1) — fail-closed, see Subscription#matches?. `types:` (nil =
|
|
122
|
+
# any) filters by event type so a subscriber's queue only ever holds what
|
|
123
|
+
# its consumer answers (WS6).
|
|
124
|
+
def subscribe(task_id: nil, session_id: nil, tenant: nil, types: nil)
|
|
125
|
+
sub = Subscription.new(task_id: task_id, session_id: session_id, tenant: tenant,
|
|
126
|
+
types: types,
|
|
109
127
|
on_close: ->(s) { @subscriptions.delete(s) })
|
|
110
128
|
@subscriptions << sub
|
|
111
129
|
sub
|