insika 0.1.0 → 0.3.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 +199 -5
- data/README.md +8 -2
- data/bin/insika +231 -13
- data/docs/AGENTS.md +505 -6
- data/docs/API.md +56 -0
- data/docs/CHANNELS.md +100 -10
- data/docs/CONTEXT.md +147 -19
- data/docs/DEPLOY.md +34 -11
- data/docs/EMBEDDING.md +11 -7
- data/docs/EVALS.md +20 -1
- data/docs/FACTS.md +135 -0
- data/docs/HARVEST.md +117 -0
- data/docs/LOADTEST.md +17 -10
- data/docs/OBSERVABILITY.md +65 -2
- data/docs/REFINEMENT.md +9 -9
- data/docs/RELEASING.md +34 -7
- data/docs/RUNNING-LOCAL.md +4 -4
- data/docs/SECURITY.md +85 -11
- data/docs/SKILLS.md +189 -3
- data/docs/SOAK.md +127 -0
- data/docs/TOOLS.md +70 -2
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/domain.md +115 -0
- data/docs/index.md +2 -2
- data/docs/onboarding/start.md +1 -1
- data/lib/insika/agent_profile.rb +228 -26
- data/lib/insika/alert_dispatcher.rb +139 -0
- data/lib/insika/balloon_splitter.rb +102 -0
- data/lib/insika/baseline_store.rb +2 -2
- data/lib/insika/budget_ledger.rb +166 -0
- data/lib/insika/cache_series_store.rb +49 -0
- data/lib/insika/channel_delivery.rb +132 -24
- data/lib/insika/channel_registry.rb +1 -1
- data/lib/insika/channels/relay.rb +80 -6
- data/lib/insika/channels/web/widget.js +2 -2
- data/lib/insika/channels/web.rb +9 -9
- data/lib/insika/channels/webhook.rb +58 -0
- data/lib/insika/chat_builder.rb +145 -13
- data/lib/insika/checkpoint_store.rb +16 -0
- data/lib/insika/circuit_state.rb +114 -0
- data/lib/insika/coercion.rb +8 -0
- data/lib/insika/commands/agent_payload.rb +6 -4
- data/lib/insika/commands/cancel_followup.rb +49 -0
- 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/delete_tenant_data.rb +95 -0
- data/lib/insika/commands/export_customer_memory.rb +48 -0
- data/lib/insika/commands/forget_customer.rb +117 -0
- data/lib/insika/commands/freeze_funnel_baseline.rb +113 -0
- data/lib/insika/commands/gate_harvest.rb +138 -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/judge_shadow_pairs.rb +124 -0
- data/lib/insika/commands/memory_forget_fact.rb +20 -4
- data/lib/insika/commands/memory_put_fact.rb +23 -4
- data/lib/insika/commands/promote_harvest.rb +130 -0
- data/lib/insika/commands/record_outcome.rb +46 -0
- data/lib/insika/commands/record_shadow_reply.rb +68 -0
- data/lib/insika/commands/reject_harvest.rb +38 -0
- data/lib/insika/commands/resolve_proposal.rb +108 -0
- data/lib/insika/commands/resolve_refinement.rb +1 -1
- data/lib/insika/commands/revoke_contact.rb +49 -0
- data/lib/insika/commands/revoke_token.rb +39 -0
- data/lib/insika/commands/rollback_harvest.rb +86 -0
- data/lib/insika/commands/rotate_tenant_token.rb +43 -0
- data/lib/insika/commands/run_distillation.rb +186 -0
- data/lib/insika/commands/run_harvest.rb +393 -0
- data/lib/insika/commands/run_refinement.rb +5 -5
- data/lib/insika/commands/send_message.rb +112 -15
- data/lib/insika/commands/session_purge.rb +67 -0
- 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/contact_store.rb +183 -0
- data/lib/insika/context/builder.rb +23 -5
- data/lib/insika/context/fragment.rb +31 -3
- data/lib/insika/context/priority.rb +6 -2
- data/lib/insika/context/provider.rb +17 -3
- data/lib/insika/context/providers/briefing.rb +96 -0
- data/lib/insika/context/providers/memory.rb +16 -7
- data/lib/insika/context/providers/prompt.rb +30 -2
- 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 +7 -1
- data/lib/insika/context/providers/skill_trigger.rb +128 -0
- data/lib/insika/context/providers/tool_search.rb +2 -0
- data/lib/insika/context_trace_store.rb +128 -0
- data/lib/insika/delegation_store.rb +2 -2
- data/lib/insika/distill.rb +224 -0
- data/lib/insika/distill_engine.rb +169 -0
- data/lib/insika/doctor.rb +962 -7
- data/lib/insika/dsl/runtime.rb +20 -11
- data/lib/insika/dsl/server_boot.rb +74 -4
- data/lib/insika/dsl/system.rb +1 -1
- data/lib/insika/dsl.rb +152 -15
- data/lib/insika/edge_limiter.rb +167 -8
- data/lib/insika/egress_guard.rb +3 -3
- data/lib/insika/env_schema.rb +22 -12
- data/lib/insika/errors.rb +72 -5
- data/lib/insika/evals/assertions.rb +15 -14
- 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 +21 -9
- 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/evidence.rb +183 -0
- data/lib/insika/executor.rb +1092 -160
- data/lib/insika/followup_engine.rb +207 -0
- data/lib/insika/followup_policy.rb +221 -0
- data/lib/insika/followup_store.rb +306 -0
- data/lib/insika/frontmatter.rb +1 -1
- data/lib/insika/funnel_declaration.rb +106 -0
- data/lib/insika/funnel_fold.rb +179 -0
- data/lib/insika/funnel_store.rb +163 -0
- data/lib/insika/golden_store.rb +3 -3
- data/lib/insika/grounding/matcher.rb +69 -0
- data/lib/insika/grounding.rb +44 -0
- data/lib/insika/harvest/conversion_gate.rb +159 -0
- data/lib/insika/harvest/criterion.rb +98 -0
- data/lib/insika/harvest/gate.rb +194 -0
- data/lib/insika/harvest/negative_list.rb +199 -0
- data/lib/insika/harvest.rb +241 -0
- data/lib/insika/harvest_engine.rb +193 -0
- data/lib/insika/harvest_store.rb +548 -0
- 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/media.rb +298 -0
- data/lib/insika/memory_audit_store.rb +85 -0
- data/lib/insika/memory_store.rb +264 -23
- data/lib/insika/message_origin.rb +8 -3
- data/lib/insika/model_resolver.rb +1 -1
- data/lib/insika/model_selection.rb +5 -4
- data/lib/insika/model_visible.rb +87 -0
- data/lib/insika/model_visible_trace_store.rb +66 -0
- data/lib/insika/onboarding.rb +8 -3
- data/lib/insika/outbox_store.rb +44 -6
- data/lib/insika/outcome_store.rb +147 -0
- 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/packaging.rb +163 -0
- data/lib/insika/parity/criterion.rb +79 -0
- data/lib/insika/parity/verdict.rb +318 -0
- 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/prefix_fingerprint.rb +58 -0
- data/lib/insika/profile_source.rb +34 -7
- data/lib/insika/proposal_store.rb +271 -0
- data/lib/insika/provider_error_classifier.rb +160 -0
- data/lib/insika/queue_policy.rb +6 -3
- 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 +10 -10
- data/lib/insika/refinement_store.rb +12 -12
- data/lib/insika/reliability.rb +211 -0
- data/lib/insika/retention.rb +281 -0
- data/lib/insika/routing.rb +101 -0
- data/lib/insika/safety/config.rb +46 -6
- data/lib/insika/safety/corpus.rb +255 -0
- data/lib/insika/safety/detectors.rb +34 -115
- data/lib/insika/safety/factory.rb +18 -5
- data/lib/insika/safety/grounding_enforcer.rb +59 -0
- data/lib/insika/safety/grounding_validator.rb +49 -0
- data/lib/insika/safety/input_guardrail.rb +20 -5
- data/lib/insika/safety/moderator.rb +19 -11
- data/lib/insika/safety/output_filter.rb +10 -6
- data/lib/insika/safety/output_validator.rb +13 -7
- 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/schema_guard.rb +35 -0
- data/lib/insika/server/app.rb +366 -54
- data/lib/insika/server/boot.rb +4 -4
- data/lib/insika/server/rack_app.rb +31 -7
- data/lib/insika/server/responses.rb +58 -9
- data/lib/insika/server/tenant_auth.rb +61 -0
- data/lib/insika/session_actor.rb +11 -7
- data/lib/insika/session_store.rb +66 -3
- data/lib/insika/settings_store.rb +15 -5
- data/lib/insika/shadow_pair_store.rb +258 -0
- data/lib/insika/shutdown.rb +4 -4
- data/lib/insika/skill_catalog.rb +131 -20
- data/lib/insika/skill_store.rb +70 -22
- data/lib/insika/soak/envelope.rb +140 -0
- data/lib/insika/soak/report.rb +392 -0
- data/lib/insika/soak/runner.rb +554 -0
- data/lib/insika/steer_injector.rb +1 -1
- data/lib/insika/store.rb +11 -2
- data/lib/insika/stores/memory.rb +6 -0
- data/lib/insika/stores/sqlite.rb +8 -0
- data/lib/insika/studio/app.rb +1058 -75
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +27 -26
- data/lib/insika/studio/assets/dist/favicon.svg +6 -0
- data/lib/insika/studio/forms.rb +274 -22
- data/lib/insika/studio/nav_icons.rb +7 -2
- data/lib/insika/studio/views/_message.erb +2 -2
- data/lib/insika/studio/views/agent_detail.erb +629 -86
- data/lib/insika/studio/views/agents.erb +11 -7
- data/lib/insika/studio/views/approvals.erb +4 -1
- data/lib/insika/studio/views/chats.erb +4 -1
- data/lib/insika/studio/views/customer.erb +94 -0
- data/lib/insika/studio/views/customers.erb +32 -0
- data/lib/insika/studio/views/evals.erb +4 -1
- data/lib/insika/studio/views/facts.erb +133 -0
- data/lib/insika/studio/views/followups.erb +125 -0
- data/lib/insika/studio/views/funnel.erb +106 -0
- data/lib/insika/studio/views/harvest.erb +234 -0
- data/lib/insika/studio/views/home.erb +2 -1
- data/lib/insika/studio/views/layout.erb +1 -0
- data/lib/insika/studio/views/parity.erb +147 -0
- data/lib/insika/studio/views/playground.erb +7 -1
- data/lib/insika/studio/views/refinement.erb +4 -4
- data/lib/insika/studio/views/session.erb +133 -3
- data/lib/insika/studio/views/settings.erb +9 -12
- data/lib/insika/studio/views/skills.erb +66 -12
- data/lib/insika/studio/views/system_files.erb +1 -1
- data/lib/insika/studio/views/task.erb +13 -0
- data/lib/insika/studio/views/tasks.erb +4 -1
- data/lib/insika/studio/views/tools.erb +0 -1
- data/lib/insika/subagent_graph.rb +3 -3
- data/lib/insika/task_actor.rb +3 -3
- data/lib/insika/task_store.rb +22 -2
- 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 +54 -33
- data/lib/insika/tick.rb +146 -0
- data/lib/insika/token_store.rb +168 -0
- data/lib/insika/tool_assembly.rb +5 -5
- data/lib/insika/tool_definition.rb +25 -15
- data/lib/insika/tool_envelope.rb +70 -1
- data/lib/insika/tool_manifest.rb +11 -7
- 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 +14 -5
- data/lib/insika/tools/generate_image.rb +44 -0
- data/lib/insika/tools/load_skill.rb +61 -3
- data/lib/insika/tools/schedule_followup.rb +164 -0
- 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/tools/tts.rb +47 -0
- data/lib/insika/tools/update_briefing.rb +126 -0
- data/lib/insika/turn_output.rb +2 -2
- data/lib/insika/turn_state.rb +54 -13
- data/lib/insika/turn_timing.rb +24 -4
- data/lib/insika/usage_ledger.rb +1 -1
- data/lib/insika/version.rb +1 -1
- data/lib/insika/vitals.rb +84 -0
- data/lib/insika/wiring/graph.rb +372 -34
- data/lib/insika/workflow.rb +1 -1
- data/lib/insika/workflow_registry.rb +1 -1
- data/lib/insika.rb +122 -16
- metadata +95 -2
- data/lib/insika/server/admin_auth.rb +0 -29
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Parity
|
|
7
|
+
# Both halves of an exchange formatted IDENTICALLY, so the judge cannot tell
|
|
8
|
+
# them apart by shape (anonymity by construction).
|
|
9
|
+
def self.transcript(inbound, reply)
|
|
10
|
+
"customer: #{inbound.to_s.strip}\nassistant: #{reply.to_s.strip}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# C6 — the mechanical fold. Given the window's pairs and the frozen criterion,
|
|
14
|
+
# produce ONE verdict and show its arithmetic. Pure: no store, no clock beyond
|
|
15
|
+
# the injected `now`, no LLM. Every number the Studio prints comes from here,
|
|
16
|
+
# so two people reading the same pairs get the same answer (E3).
|
|
17
|
+
#
|
|
18
|
+
# The order of the steps IS the rule:
|
|
19
|
+
# window -> pre-registration -> buckets -> volume -> sanity -> primary -> guards
|
|
20
|
+
module Verdict
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
Report = Data.define(
|
|
24
|
+
:verdict, # :pass | :fail | :insufficient | :invalid
|
|
25
|
+
:window, # { from:, to:, days: }
|
|
26
|
+
:counts, # judged/decided/better/comparable/worse/split/unknown/
|
|
27
|
+
# silent/incomplete/human_assisted/open
|
|
28
|
+
:daily, # [{ date:, pairs:, decided: }]
|
|
29
|
+
:per_agent, # { agent => { decided:, win_or_tie:, worse:, meets: } }
|
|
30
|
+
:win_or_tie, :win_or_tie_lower, :worse_rate,
|
|
31
|
+
:undecided_rate, :incomplete_rate,
|
|
32
|
+
:checks, # [{ id:, met:, actual:, required:, note: }]
|
|
33
|
+
:criterion_sha, :reason
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
DECIDED = %w[better comparable worse].freeze
|
|
37
|
+
|
|
38
|
+
Check = Data.define(:id, :met, :actual, :required, :note)
|
|
39
|
+
|
|
40
|
+
def fold(pairs:, criterion:, now: Time.now.utc)
|
|
41
|
+
rule = criterion.rule
|
|
42
|
+
from = now - (rule.window_days * 86_400)
|
|
43
|
+
windowed = Array(pairs).select { |p| in_window?(p, from, now) }
|
|
44
|
+
|
|
45
|
+
checks = []
|
|
46
|
+
|
|
47
|
+
# 2. Pre-registration: a window whose pairs disagree about which file was
|
|
48
|
+
# frozen is :invalid — a post-hoc edit produces NO verdict, not a worse one.
|
|
49
|
+
sha = sha_check(windowed, criterion.sha)
|
|
50
|
+
if sha && !sha.met
|
|
51
|
+
checks << sha
|
|
52
|
+
return build(verdict: :invalid, criterion: criterion, from: from, to: now,
|
|
53
|
+
rule: rule, counts: nil, daily: [], per_agent: {},
|
|
54
|
+
checks: checks, reason: sha.note)
|
|
55
|
+
end
|
|
56
|
+
checks << sha if sha
|
|
57
|
+
|
|
58
|
+
# 3. Buckets: decided is better+comparable+worse against the incumbent's
|
|
59
|
+
# model (`vs: agent`). human-assisted/silent/incomplete/open/split/unknown
|
|
60
|
+
# are counted and reported, never in the denominator.
|
|
61
|
+
counts = count(windowed)
|
|
62
|
+
daily = daily_of(windowed, from, now, rule)
|
|
63
|
+
per_agent = per_agent_of(windowed)
|
|
64
|
+
|
|
65
|
+
# 4. Volume: every day at the floor, and enough decided pairs in total —
|
|
66
|
+
# short volume is :insufficient (an honest "keep running"), never a fail.
|
|
67
|
+
volume = volume_check(daily, rule)
|
|
68
|
+
checks << volume
|
|
69
|
+
decided_check = Check.new(id: :min_decided, met: counts[:decided] >= rule.min_decided,
|
|
70
|
+
actual: counts[:decided], required: rule.min_decided,
|
|
71
|
+
note: "decided #{counts[:decided]} < #{rule.min_decided}")
|
|
72
|
+
checks << decided_check
|
|
73
|
+
unless volume.met && decided_check.met
|
|
74
|
+
notes = [volume.met ? nil : volume.note, decided_check.met ? nil : decided_check.note].compact
|
|
75
|
+
return build(verdict: :insufficient, criterion: criterion, from: from, to: now,
|
|
76
|
+
rule: rule, counts: counts, daily: daily, per_agent: per_agent,
|
|
77
|
+
checks: checks, reason: notes.join("; "))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# 5. Sanity: a panel that cannot decide, or a mirror that cannot pair,
|
|
81
|
+
# is not measuring anything -> :invalid. The denominator is the panel's
|
|
82
|
+
# own work: judged pairs against a model — human-assisted pairs are
|
|
83
|
+
# counted and reported, never in a denominator.
|
|
84
|
+
judged = counts[:judged] - counts[:human_assisted]
|
|
85
|
+
undecided_rate = judged.positive? ? (counts[:split] + counts[:unknown]).fdiv(judged) : 0.0
|
|
86
|
+
undecided = Check.new(id: :undecided, met: undecided_rate <= rule.undecided_rate_ceiling,
|
|
87
|
+
actual: undecided_rate.round(4), required: "<= #{rule.undecided_rate_ceiling}",
|
|
88
|
+
note: "undecided rate #{undecided_rate.round(3)} > #{rule.undecided_rate_ceiling} " \
|
|
89
|
+
"(#{counts[:split]} split, #{counts[:unknown]} unknown of #{judged} judged)")
|
|
90
|
+
checks << undecided
|
|
91
|
+
|
|
92
|
+
non_open = windowed.length - counts[:open]
|
|
93
|
+
incomplete_rate = non_open.positive? ? counts[:incomplete].fdiv(non_open) : 0.0
|
|
94
|
+
incomplete = Check.new(id: :incomplete, met: incomplete_rate <= rule.incomplete_rate_ceiling,
|
|
95
|
+
actual: incomplete_rate.round(4), required: "<= #{rule.incomplete_rate_ceiling}",
|
|
96
|
+
note: "incomplete rate #{incomplete_rate.round(3)} > #{rule.incomplete_rate_ceiling} " \
|
|
97
|
+
"(#{counts[:incomplete]} of #{non_open} non-open pairs never got both halves)")
|
|
98
|
+
checks << incomplete
|
|
99
|
+
unless undecided.met && incomplete.met
|
|
100
|
+
notes = [undecided.met ? nil : undecided.note, incomplete.met ? nil : incomplete.note].compact
|
|
101
|
+
return build(verdict: :invalid, criterion: criterion, from: from, to: now,
|
|
102
|
+
rule: rule, counts: counts, daily: daily, per_agent: per_agent,
|
|
103
|
+
rates: { undecided: undecided_rate, incomplete: incomplete_rate },
|
|
104
|
+
checks: checks, reason: notes.join("; "))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# 6. Primary: the lower bound of the 95% Wilson interval on win-or-tie.
|
|
108
|
+
# The estimator is NAMED in the criterion, so a point estimate can never be
|
|
109
|
+
# quietly substituted.
|
|
110
|
+
win_or_tie = counts[:decided].positive? ? (counts[:better] + counts[:comparable]).fdiv(counts[:decided]) : 0.0
|
|
111
|
+
lower = wilson_lower(counts[:better] + counts[:comparable], counts[:decided])
|
|
112
|
+
primary = Check.new(id: :primary, met: lower >= rule.win_or_tie_floor,
|
|
113
|
+
actual: { win_or_tie: win_or_tie.round(4), wilson_lower_95: lower.round(4) },
|
|
114
|
+
required: "#{rule.estimator} >= #{rule.win_or_tie_floor}",
|
|
115
|
+
note: "win-or-tie #{win_or_tie.round(3)} (wilson lower #{lower.round(3)}) " \
|
|
116
|
+
"< floor #{rule.win_or_tie_floor}")
|
|
117
|
+
checks << primary
|
|
118
|
+
|
|
119
|
+
# 7. Guards: the worse tail on its own, and any store blocking its own cut.
|
|
120
|
+
worse_rate = counts[:decided].positive? ? counts[:worse].fdiv(counts[:decided]) : 0.0
|
|
121
|
+
worse = Check.new(id: :worse_rate, met: worse_rate <= rule.worse_rate_ceiling,
|
|
122
|
+
actual: worse_rate.round(4), required: "<= #{rule.worse_rate_ceiling}",
|
|
123
|
+
note: "worse rate #{worse_rate.round(3)} > #{rule.worse_rate_ceiling} " \
|
|
124
|
+
"(#{counts[:worse]} worse of #{counts[:decided]} decided)")
|
|
125
|
+
checks << worse
|
|
126
|
+
agent = per_agent_check(per_agent, rule)
|
|
127
|
+
checks << agent if agent
|
|
128
|
+
|
|
129
|
+
failed = [primary, worse, agent].compact.reject(&:met)
|
|
130
|
+
unless failed.empty?
|
|
131
|
+
return build(verdict: :fail, criterion: criterion, from: from, to: now,
|
|
132
|
+
rule: rule, counts: counts, daily: daily, per_agent: per_agent,
|
|
133
|
+
rates: { win_or_tie: win_or_tie, lower: lower, worse: worse_rate,
|
|
134
|
+
undecided: undecided_rate, incomplete: incomplete_rate },
|
|
135
|
+
checks: checks, reason: failed.map(&:note).join("; "))
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
build(verdict: :pass, criterion: criterion, from: from, to: now,
|
|
139
|
+
rule: rule, counts: counts, daily: daily, per_agent: per_agent,
|
|
140
|
+
rates: { win_or_tie: win_or_tie, lower: lower, worse: worse_rate,
|
|
141
|
+
undecided: undecided_rate, incomplete: incomplete_rate },
|
|
142
|
+
checks: checks, reason: "the cut cleared: every check in the window is green")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Wilson score interval, lower bound. Pure arithmetic, unit-tested against
|
|
146
|
+
# published values (n=210, k=183 -> ~0.819).
|
|
147
|
+
def wilson_lower(successes, n, z: 1.96)
|
|
148
|
+
return 0.0 if n.to_i <= 0
|
|
149
|
+
|
|
150
|
+
p_hat = successes.to_f / n
|
|
151
|
+
z2 = z * z
|
|
152
|
+
denominator = 1 + (z2 / n)
|
|
153
|
+
centre = p_hat + (z2 / (2 * n))
|
|
154
|
+
spread = Math.sqrt((p_hat * (1 - p_hat) + (z2 / (4 * n))) / n)
|
|
155
|
+
(centre - (z * spread)) / denominator
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# -- private helpers ---------------------------------------------------
|
|
159
|
+
|
|
160
|
+
def in_window?(pair, from, to)
|
|
161
|
+
created = pair.respond_to?(:created_at) ? pair.created_at : nil
|
|
162
|
+
time = begin
|
|
163
|
+
Time.iso8601(created.to_s)
|
|
164
|
+
rescue ArgumentError
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
time && time >= from && time < to
|
|
168
|
+
end
|
|
169
|
+
private_class_method :in_window?
|
|
170
|
+
|
|
171
|
+
# Pre-registration has nothing to stand on when NO pair carries the sha:
|
|
172
|
+
# the window predates the freeze, and stamping a verdict on it is the same
|
|
173
|
+
# post-hoc edit the mixed-sha rule exists to refuse.
|
|
174
|
+
def sha_check(pairs, expected)
|
|
175
|
+
shas = pairs.filter_map { |p| p.respond_to?(:criterion_sha) ? p.criterion_sha : nil }.compact.uniq
|
|
176
|
+
if shas.length == 1 && shas.first == expected
|
|
177
|
+
Check.new(id: :criterion_sha, met: true, actual: expected, required: expected, note: nil)
|
|
178
|
+
elsif pairs.empty?
|
|
179
|
+
# nothing to pre-register — the volume check owns the empty window
|
|
180
|
+
Check.new(id: :criterion_sha, met: true, actual: expected, required: expected, note: nil)
|
|
181
|
+
elsif shas.empty?
|
|
182
|
+
Check.new(id: :criterion_sha, met: false, actual: "no pair stamped", required: expected,
|
|
183
|
+
note: "no pair in the window carries a criterion_sha — the window predates the freeze")
|
|
184
|
+
else
|
|
185
|
+
Check.new(id: :criterion_sha, met: false, actual: shas.join(" | "), required: expected,
|
|
186
|
+
note: "window carries #{shas.join(', ')}, expected the frozen #{expected}")
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
private_class_method :sha_check
|
|
190
|
+
|
|
191
|
+
def count(windowed)
|
|
192
|
+
buckets = windowed.group_by { |p| bucket_of(p) }
|
|
193
|
+
base = { judged: windowed.count { |p| p.status == :judged },
|
|
194
|
+
open: windowed.count { |p| p.status == :open } }
|
|
195
|
+
{ silent: buckets[:silent].to_a.length,
|
|
196
|
+
incomplete: buckets[:incomplete].to_a.length,
|
|
197
|
+
human_assisted: buckets[:human_assisted].to_a.length,
|
|
198
|
+
better: buckets[:better].to_a.length, comparable: buckets[:comparable].to_a.length,
|
|
199
|
+
worse: buckets[:worse].to_a.length, split: buckets[:split].to_a.length,
|
|
200
|
+
unknown: buckets[:unknown].to_a.length }.merge(base).tap do |c|
|
|
201
|
+
c[:decided] = c[:better] + c[:comparable] + c[:worse]
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
private_class_method :count
|
|
205
|
+
|
|
206
|
+
# A malformed verdict blob counts as :unknown, never as a preference — the
|
|
207
|
+
# same rule Pairwise applies to a broken judge.
|
|
208
|
+
def bucket_of(pair)
|
|
209
|
+
return pair.status.to_sym unless pair.status == :judged
|
|
210
|
+
|
|
211
|
+
verdict = pair.respond_to?(:verdict) && pair.verdict.is_a?(Hash) ? pair.verdict : nil
|
|
212
|
+
return :unknown unless verdict
|
|
213
|
+
|
|
214
|
+
return :human_assisted if verdict["vs"] == "human-assisted"
|
|
215
|
+
|
|
216
|
+
outcome = verdict["outcome"].to_s
|
|
217
|
+
DECIDED.include?(outcome) ? outcome.to_sym : outcome == "split" ? :split : :unknown
|
|
218
|
+
end
|
|
219
|
+
private_class_method :bucket_of
|
|
220
|
+
|
|
221
|
+
# One entry per CALENDAR date the window touches, bucketed by each pair's
|
|
222
|
+
# own date — the newest pairs must land on the grid, never outside the list.
|
|
223
|
+
# `floor` is the daily requirement: pairs_per_day for a day whose full 24h
|
|
224
|
+
# lie inside the window, 0 for the two boundary days — they are partial by
|
|
225
|
+
# construction (`from` carries now's time of day), and holding a partial day
|
|
226
|
+
# to a full floor would make the gate unreachable under constant traffic at
|
|
227
|
+
# exactly the floor. Partial days are reported, never required.
|
|
228
|
+
def daily_of(windowed, from, to, rule)
|
|
229
|
+
by_day = windowed.group_by { |p| Time.iso8601(p.created_at.to_s).utc.strftime("%Y-%m-%d") }
|
|
230
|
+
calendar_dates(from, to).map do |date|
|
|
231
|
+
pairs = by_day[date].to_a
|
|
232
|
+
decided = pairs.count { |p| DECIDED.include?(bucket_of(p).to_s) }
|
|
233
|
+
{ date: date, pairs: pairs.length, decided: decided,
|
|
234
|
+
floor: full_day?(date, from, to) ? rule.pairs_per_day : 0 }
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
private_class_method :daily_of
|
|
238
|
+
|
|
239
|
+
def calendar_dates(from, to)
|
|
240
|
+
dates = []
|
|
241
|
+
day = Time.utc(from.utc.year, from.utc.month, from.utc.day)
|
|
242
|
+
last = Time.utc(to.utc.year, to.utc.month, to.utc.day)
|
|
243
|
+
while day <= last
|
|
244
|
+
dates << day.strftime("%Y-%m-%d")
|
|
245
|
+
day += 86_400
|
|
246
|
+
end
|
|
247
|
+
dates
|
|
248
|
+
end
|
|
249
|
+
private_class_method :calendar_dates
|
|
250
|
+
|
|
251
|
+
def full_day?(date, from, to)
|
|
252
|
+
y, m, d = date.split("-").map(&:to_i)
|
|
253
|
+
day_start = Time.utc(y, m, d)
|
|
254
|
+
day_end = day_start + 86_400
|
|
255
|
+
[[to, day_end].min - [from, day_start].max, 0].max >= 86_400
|
|
256
|
+
end
|
|
257
|
+
private_class_method :full_day?
|
|
258
|
+
|
|
259
|
+
def volume_check(daily, rule)
|
|
260
|
+
short = daily.find { |d| d[:floor].positive? && d[:pairs] < d[:floor] }
|
|
261
|
+
full_days = daily.count { |d| d[:floor].positive? }
|
|
262
|
+
return Check.new(id: :volume, met: true,
|
|
263
|
+
actual: "#{full_days} full day(s) at >= #{rule.pairs_per_day}",
|
|
264
|
+
required: "#{rule.pairs_per_day} pairs on every full day",
|
|
265
|
+
note: nil) unless short
|
|
266
|
+
|
|
267
|
+
Check.new(id: :volume, met: false,
|
|
268
|
+
actual: "#{short[:date]}: #{short[:pairs]} pairs",
|
|
269
|
+
required: "#{short[:floor]} pairs that day",
|
|
270
|
+
note: "day #{short[:date]} has #{short[:pairs]} pairs (floor #{short[:floor]})")
|
|
271
|
+
end
|
|
272
|
+
private_class_method :volume_check
|
|
273
|
+
|
|
274
|
+
def per_agent_of(windowed)
|
|
275
|
+
windowed.select { |p| DECIDED.include?(bucket_of(p).to_s) }
|
|
276
|
+
.group_by { |p| p.agent.to_s }
|
|
277
|
+
.transform_values do |pairs|
|
|
278
|
+
decided = pairs.length
|
|
279
|
+
win_or_tie = decided.positive? ? pairs.count { |p| %w[better comparable].include?(bucket_of(p).to_s) }.fdiv(decided) : 0.0
|
|
280
|
+
{ decided: decided, win_or_tie: win_or_tie.round(4),
|
|
281
|
+
worse: pairs.count { |p| bucket_of(p) == :worse }, meets: nil }
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
private_class_method :per_agent_of
|
|
285
|
+
|
|
286
|
+
# A store with enough decided pairs blocks its own cut when its win-or-tie
|
|
287
|
+
# sits under its own floor — even if the aggregate passes.
|
|
288
|
+
def per_agent_check(per_agent, rule)
|
|
289
|
+
blockers = per_agent.each_with_object({}) do |(agent, stats), acc|
|
|
290
|
+
next if stats[:decided] < rule.per_agent_min_decided
|
|
291
|
+
|
|
292
|
+
stats[:meets] = stats[:win_or_tie] >= rule.per_agent_win_or_tie_floor
|
|
293
|
+
acc[agent] = stats[:win_or_tie] unless stats[:meets]
|
|
294
|
+
end
|
|
295
|
+
return nil if blockers.empty?
|
|
296
|
+
|
|
297
|
+
Check.new(id: :per_agent, met: false, actual: blockers.map { |a, r| "#{a}: #{r}" }.join(", "),
|
|
298
|
+
required: ">= #{rule.per_agent_win_or_tie_floor} for stores with >= #{rule.per_agent_min_decided} decided",
|
|
299
|
+
note: blockers.map { |a, r| "#{a} sits at #{r.round(3)} (< #{rule.per_agent_win_or_tie_floor})" }.join("; "))
|
|
300
|
+
end
|
|
301
|
+
private_class_method :per_agent_check
|
|
302
|
+
|
|
303
|
+
def build(verdict:, criterion:, from:, to:, rule:, counts:, daily:, per_agent:, checks:, reason:, rates: nil)
|
|
304
|
+
r = rates || {}
|
|
305
|
+
Report.new(
|
|
306
|
+
verdict: verdict,
|
|
307
|
+
window: { from: from.utc.iso8601, to: to.utc.iso8601, days: rule.window_days },
|
|
308
|
+
counts: counts, daily: daily, per_agent: per_agent,
|
|
309
|
+
win_or_tie: r[:win_or_tie]&.round(4), win_or_tie_lower: r[:lower]&.round(4),
|
|
310
|
+
worse_rate: r[:worse]&.round(4),
|
|
311
|
+
undecided_rate: r[:undecided]&.round(4), incomplete_rate: r[:incomplete]&.round(4),
|
|
312
|
+
checks: checks, criterion_sha: criterion.sha, reason: reason
|
|
313
|
+
)
|
|
314
|
+
end
|
|
315
|
+
private_class_method :build
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
@@ -64,7 +64,7 @@ module Insika
|
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
# -> [PendingAction] every :pending across all tasks — the approvals inbox
|
|
67
|
-
# (
|
|
67
|
+
# (Studio). Single O(n) scan (vs. open_for per task = O(n·m)); the
|
|
68
68
|
# UI resolves task context afterwards via TaskStore#find.
|
|
69
69
|
def all_open
|
|
70
70
|
@store.list(SCOPE, KEY_PREFIX).filter_map do |key|
|
data/lib/insika/plugin/loader.rb
CHANGED
|
@@ -209,7 +209,7 @@ module Insika
|
|
|
209
209
|
@registries[:workflows].register(name, callable, plugin: @plugin_id, &block)
|
|
210
210
|
end
|
|
211
211
|
|
|
212
|
-
#
|
|
212
|
+
# a channel is an INSTANCE (it holds its credentials and its
|
|
213
213
|
# HTTP client), so unlike a tool there is no factory to defer. Declared-or-
|
|
214
214
|
# ignored like the others: the id is a URL segment, and a plugin quietly
|
|
215
215
|
# mounting a route nobody declared is exactly what the contract list prevents.
|
|
@@ -254,7 +254,7 @@ module Insika
|
|
|
254
254
|
|
|
255
255
|
if kind == :workflow
|
|
256
256
|
warn "[plugin #{@plugin_id}] capability '#{name}' (kind: workflow) registered without a consumer " \
|
|
257
|
-
"in this slice — agent exposure is follow-up
|
|
257
|
+
"in this slice — agent exposure is follow-up"
|
|
258
258
|
end
|
|
259
259
|
|
|
260
260
|
@staged_capabilities << { capability: name, impl_name: impl_name, kind: kind,
|
data/lib/insika/policy/policy.rb
CHANGED
|
@@ -47,7 +47,7 @@ module Insika
|
|
|
47
47
|
# Absorbs ToolRegistry#resolve as a policy: optional without
|
|
48
48
|
# opt-in -> deny; tools_deny -> deny ("deny always wins"); tools_allow
|
|
49
49
|
# with the semantics (nil = all; [] = ∅; [names] = final set).
|
|
50
|
-
#
|
|
50
|
+
# `tools_allow_groups` UNIONS in the groups' tools
|
|
51
51
|
# (the group is GIVEN in the Entry metadata). Both allowlists nil = all.
|
|
52
52
|
class ToolAllowlist < Base
|
|
53
53
|
def decide(request)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest/sha2"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
# — the cache-prefix hash chain. Inputs are the SYSTEM-placement
|
|
7
|
+
# fragments in canonical render order (the Builder's sort, C3) and the tool
|
|
8
|
+
# schema serialization — the same yardstick the token estimate uses
|
|
9
|
+
# (name + description + parameters.inspect, executor.rb:957). Outputs are
|
|
10
|
+
# PII-free digests: it never sees message text, only hashes leave this class.
|
|
11
|
+
# Pure stdlib (digest/sha2), no gem, no IO.
|
|
12
|
+
class PrefixFingerprint
|
|
13
|
+
# -> { "prompt" => "sha256…", …, "tool_schemas" => "sha256…",
|
|
14
|
+
# "prefix" => "sha256…" }
|
|
15
|
+
# Keys are the demodulized, downcased provider ids, in render order.
|
|
16
|
+
# A category with no fragments is absent (not an empty hash).
|
|
17
|
+
# The category digest = SHA256 of the category's fragments joined "\n\n"
|
|
18
|
+
# (the Builder's separator — the digest matches rendered bytes).
|
|
19
|
+
# "prefix" = SHA256 of the category digests concatenated IN CHAIN ORDER
|
|
20
|
+
# (identity categories, then volatile, then tool_schemas) — any divergence
|
|
21
|
+
# anywhere above the boundary changes it.
|
|
22
|
+
def self.compute(system_fragments, tool_serial:)
|
|
23
|
+
digests = {} # category name -> digest, insertion = render order
|
|
24
|
+
grouped = system_fragments.group_by { |f| category(f.source) }
|
|
25
|
+
grouped.each do |name, frags|
|
|
26
|
+
digests[name] = digest(frags.map(&:content).join("\n\n"))
|
|
27
|
+
end
|
|
28
|
+
digests["tool_schemas"] = digest(tool_serial.to_s) unless tool_serial.nil?
|
|
29
|
+
digests["prefix"] = digest(digests.values.join) unless digests.empty?
|
|
30
|
+
digests
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# -> String | nil. The first category (in CURRENT chain order) whose digest
|
|
34
|
+
# differs or is absent from `previous`; else the first PREVIOUS key now
|
|
35
|
+
# absent from the current chain (a vanished block is a divergence too);
|
|
36
|
+
# else nil. nil `previous` (first turn) -> nil. The returned name is a
|
|
37
|
+
# category id — PII-free by construction.
|
|
38
|
+
#
|
|
39
|
+
# The cumulative "prefix" key is deliberately SKIPPED in the scan: its
|
|
40
|
+
# digest changes whenever ANY category moves, so scanning it would shadow a
|
|
41
|
+
# vanished block (every surviving category matches, "prefix" differs, and
|
|
42
|
+
# the vanished-fallback below becomes unreachable — reporting `broke:
|
|
43
|
+
# prefix` instead of the category that actually left).
|
|
44
|
+
def self.invalidation_reason(current, previous)
|
|
45
|
+
return nil unless previous.is_a?(Hash)
|
|
46
|
+
|
|
47
|
+
current.each_key do |name|
|
|
48
|
+
next if name == "prefix"
|
|
49
|
+
|
|
50
|
+
return name unless previous[name] == current[name]
|
|
51
|
+
end
|
|
52
|
+
(previous.keys - current.keys).first
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.category(source) = source.to_s.split("::").last.to_s.downcase
|
|
56
|
+
def self.digest(bytes) = Digest::SHA256.hexdigest(bytes)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -90,6 +90,7 @@ module Insika
|
|
|
90
90
|
tools_allow: h[:tools_allow], tools_deny: h[:tools_deny] || [],
|
|
91
91
|
tools_allow_groups: h[:tools_allow_groups],
|
|
92
92
|
skills: h[:skills],
|
|
93
|
+
skills_eager: h[:skills_eager],
|
|
93
94
|
context_providers: h[:context_providers],
|
|
94
95
|
workflows_allow: h[:workflows_allow],
|
|
95
96
|
policies: Array(h[:policies]).map(&:to_sym),
|
|
@@ -97,31 +98,57 @@ module Insika
|
|
|
97
98
|
limits: symbolize_limits(h[:limits]),
|
|
98
99
|
approvals_required: h[:approvals_required],
|
|
99
100
|
capabilities: h[:capabilities],
|
|
100
|
-
# subagents
|
|
101
|
+
# subagents: allowlist of child ids; build re-normalizes to
|
|
101
102
|
# [String]. nil round-trips as nil (opt-in: NONE).
|
|
102
103
|
subagents: h[:subagents],
|
|
103
104
|
tools_deferred: h[:tools_deferred],
|
|
104
105
|
memory: h[:memory],
|
|
105
106
|
prompt_caching: h[:prompt_caching],
|
|
106
|
-
#
|
|
107
|
+
# tool_persistence: the one opt-out flag — nil round-trips as nil (= ON);
|
|
108
|
+
# only a stored explicit false turns the discipline block off.
|
|
109
|
+
tool_persistence: h[:tool_persistence],
|
|
110
|
+
tool_output_compression: h[:tool_output_compression],
|
|
111
|
+
# params/model_policy: the resolver tolerates string keys from
|
|
107
112
|
# the JSON round-trip (ModelResolver#normalize_params / ModelPolicy), so no
|
|
108
113
|
# re-symbolization needed here.
|
|
109
114
|
params: h[:params] || {},
|
|
110
115
|
model_policy: h[:model_policy],
|
|
111
|
-
|
|
116
|
+
budget: h[:budget],
|
|
117
|
+
reliability: h[:reliability],
|
|
118
|
+
alerts: h[:alerts],
|
|
119
|
+
routes: h[:routes],
|
|
120
|
+
stuck_signal: h[:stuck_signal],
|
|
121
|
+
outputs: h[:outputs],
|
|
122
|
+
briefing_fields: h[:briefing_fields],
|
|
123
|
+
# grounding profile data — a plain Hash read with string keys
|
|
124
|
+
# by Insika::Grounding.parse per turn; nil round-trips as nil (= off).
|
|
125
|
+
grounding: h[:grounding],
|
|
126
|
+
# guardrails: a plain Hash; Safety::Config tolerates the JSON
|
|
112
127
|
# round-trip (string keys/values), so no re-symbolization here.
|
|
113
128
|
guardrails: h[:guardrails],
|
|
114
|
-
# sandbox
|
|
129
|
+
# sandbox: a plain config Hash; Sandbox.build tolerates the JSON
|
|
115
130
|
# round-trip (string keys), so no re-symbolization here. nil = absent.
|
|
116
131
|
sandbox: h[:sandbox],
|
|
117
|
-
# refinement
|
|
132
|
+
# refinement: a plain config Hash read with string keys by the
|
|
118
133
|
# RunRefinement handler; nil round-trips as nil (= report-only).
|
|
119
134
|
refinement: h[:refinement],
|
|
120
|
-
# capabilities_declared
|
|
135
|
+
# capabilities_declared: flat [String]; build re-normalizes.
|
|
121
136
|
capabilities_declared: h[:capabilities_declared],
|
|
122
137
|
# edge_stream: which internal channels may cross to the customer. {} = neither.
|
|
123
138
|
edge_stream: h[:edge_stream],
|
|
124
|
-
metadata: h[:metadata] || {}
|
|
139
|
+
metadata: h[:metadata] || {},
|
|
140
|
+
# funnel declaration — a plain Hash read with string keys by
|
|
141
|
+
# FunnelDeclaration.parse; nil round-trips as nil (= no funnel).
|
|
142
|
+
funnel: h[:funnel],
|
|
143
|
+
# followup declaration — a plain Hash read with string keys by
|
|
144
|
+
# FollowupPolicy.parse; nil round-trips as nil (= feature off).
|
|
145
|
+
followup: h[:followup],
|
|
146
|
+
# distill declaration — a plain Hash read with string keys by
|
|
147
|
+
# the distiller/engine/doctor; nil round-trips as nil (= feature off).
|
|
148
|
+
distill: h[:distill],
|
|
149
|
+
# harvest declaration — a plain Hash read with string keys
|
|
150
|
+
# by the miner/engine/doctor; nil round-trips as nil (= loop off).
|
|
151
|
+
harvest: h[:harvest]
|
|
125
152
|
)
|
|
126
153
|
end
|
|
127
154
|
|