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
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Insika
|
|
4
4
|
module Refinement
|
|
5
|
-
# Scores a candidate by RUNNING it
|
|
6
|
-
# the edit looks good — that measures nothing, and
|
|
5
|
+
# Scores a candidate by RUNNING it. Not by asking a model whether
|
|
6
|
+
# the edit looks good — that measures nothing, and says so in one line.
|
|
7
7
|
#
|
|
8
8
|
# 1. clone the agent into a throwaway id (`<agent>-cand-<run8>`)
|
|
9
9
|
# 2. copy its instruction files, apply the candidate's edits to the COPY
|
|
@@ -27,7 +27,7 @@ module Insika
|
|
|
27
27
|
# it — nil when no turn carried usage. `cached` is how much of that came from
|
|
28
28
|
# the prompt cache, kept separate because it is what explains one candidate
|
|
29
29
|
# costing 8× another over the same cases. `tokens` is what the panel's budget
|
|
30
|
-
#
|
|
30
|
+
# spends and what the operator reads on the run: a gate is the expensive
|
|
31
31
|
# half of refinement and a loop whose cost is invisible is one nobody can decide
|
|
32
32
|
# to keep.
|
|
33
33
|
Report = Data.define(:candidate_id, :passed, :reason, :cases, :passed_cases,
|
|
@@ -48,9 +48,9 @@ module Insika
|
|
|
48
48
|
# credential the operator can rotate.
|
|
49
49
|
# capabilities_factory: -> an `Evals::HttpCapabilities` for the clone, or nil.
|
|
50
50
|
# Without it a case whose `requires` the agent cannot satisfy RUNS and fails
|
|
51
|
-
# (
|
|
51
|
+
# (says it must skip) — and then the gate and `evals/run.rb`, the
|
|
52
52
|
# two callers of the one evaluator, disagree about what the corpus even
|
|
53
|
-
# measures.
|
|
53
|
+
# measures. exists to prevent exactly that.
|
|
54
54
|
def initialize(profiles:, agent_files:, goldens:, baselines:, transport_factory:,
|
|
55
55
|
capabilities_factory: nil, judge_factory: nil, tolerance: DEFAULT_TOLERANCE)
|
|
56
56
|
@profiles = profiles
|
|
@@ -68,7 +68,7 @@ module Insika
|
|
|
68
68
|
# not gate is more useful than an exception in a log.
|
|
69
69
|
def score(agent_id:, candidate:, run_id:, tolerance: nil)
|
|
70
70
|
cases = @goldens.for_agent(agent_id)
|
|
71
|
-
return refusal(candidate, "the agent has no golden cases — nothing to gate against
|
|
71
|
+
return refusal(candidate, "the agent has no golden cases — nothing to gate against") if cases.empty?
|
|
72
72
|
|
|
73
73
|
baseline = @baselines.get(agent_id)
|
|
74
74
|
# Without an accepted state, `Baseline.compare` compares nothing and reports
|
|
@@ -109,7 +109,7 @@ module Insika
|
|
|
109
109
|
# Measured, not reasoned: gating the real pilot agent with `settings["evals"]`
|
|
110
110
|
# unset reported **6/6, no regression** against a baseline the same corpus had
|
|
111
111
|
# just scored **2/6** — `produto-sem-cep` was judged 0.0 and "passed". Both
|
|
112
|
-
# candidates on the panel cleared. That is
|
|
112
|
+
# candidates on the panel cleared. That is's failure exactly: the CLI and
|
|
113
113
|
# the gate, the two callers of the one evaluator, disagreeing about what the
|
|
114
114
|
# corpus measures.
|
|
115
115
|
judge = @judge_factory&.call
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Insika
|
|
4
4
|
module Refinement
|
|
5
|
-
# What a refinement run is allowed to SPEND
|
|
5
|
+
# What a refinement run is allowed to SPEND.
|
|
6
6
|
#
|
|
7
7
|
# A panel of 3 proposers over a 7-case golden set is 3 model calls plus 21
|
|
8
8
|
# replayed conversations, each a real turn with real tools. That is the honest
|
|
@@ -52,15 +52,15 @@ module Insika
|
|
|
52
52
|
"unmetered" => @unmetered }
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
# The proposer PANEL
|
|
55
|
+
# The proposer PANEL: N models write N independent
|
|
56
56
|
# candidates, the gate scores each one by replaying the golden set, and the best
|
|
57
57
|
# SURVIVOR becomes the proposal a human is asked about.
|
|
58
58
|
#
|
|
59
59
|
# Independent, not consensus-seeking. Two models agreeing on wording is weak
|
|
60
|
-
# evidence and a golden case passing is strong evidence
|
|
60
|
+
# evidence and a golden case passing is strong evidence, so convergence only
|
|
61
61
|
# ever breaks a tie between candidates the gate already ranked equal.
|
|
62
62
|
#
|
|
63
|
-
# A panel of one is
|
|
63
|
+
# A panel of one is unchanged, which is why there is no second code path:
|
|
64
64
|
# `refinement.proposer` (a single ref) resolves to a one-element panel.
|
|
65
65
|
class Panel
|
|
66
66
|
# One member of the panel: the candidate, WHO wrote it (more than one model when
|
|
@@ -80,7 +80,7 @@ module Insika
|
|
|
80
80
|
# gate: a Refinement::Gate (anything answering #score).
|
|
81
81
|
# proposers: [Refinement::Proposer], already resolved by ProposerFactory.panel.
|
|
82
82
|
# budget: a Budget. The default is unlimited — a deployment that configured
|
|
83
|
-
# none gets
|
|
83
|
+
# none gets behaviour, which had no ceiling either.
|
|
84
84
|
def initialize(gate:, proposers: [], budget: Budget.new, fan_out: nil)
|
|
85
85
|
@gate = gate
|
|
86
86
|
@proposers = Array(proposers)
|
|
@@ -112,7 +112,7 @@ module Insika
|
|
|
112
112
|
# -> the best SURVIVOR, or nil when none passed.
|
|
113
113
|
#
|
|
114
114
|
# Highest graded score first; ties broken by the fewest edits (a smaller diff is
|
|
115
|
-
# a smaller bet), then by how many proposers converged on it
|
|
115
|
+
# a smaller bet), then by how many proposers converged on it. `min_by`
|
|
116
116
|
# over a negated tuple keeps the comparison in one place and stays stable, so
|
|
117
117
|
# two genuinely indistinguishable candidates resolve to the first proposer the
|
|
118
118
|
# operator listed rather than to whichever fiber finished first.
|
|
@@ -131,7 +131,7 @@ module Insika
|
|
|
131
131
|
|
|
132
132
|
private
|
|
133
133
|
|
|
134
|
-
# All proposers at once, bounded by the
|
|
134
|
+
# All proposers at once, bounded by the fan-out cap (says the
|
|
135
135
|
# panel reuses it). Each is one blocking HTTP call to a provider, so the
|
|
136
136
|
# wall-clock is the slowest model rather than their sum.
|
|
137
137
|
#
|
|
@@ -4,7 +4,7 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module Insika
|
|
6
6
|
module Refinement
|
|
7
|
-
# Writes a CANDIDATE from a run's findings
|
|
7
|
+
# Writes a CANDIDATE from a run's findings — the
|
|
8
8
|
# one place in refinement where a model is asked for anything.
|
|
9
9
|
#
|
|
10
10
|
# It is deliberately the WEAKEST link and it is built that way: everything this
|
|
@@ -180,8 +180,8 @@ module Insika
|
|
|
180
180
|
|
|
181
181
|
# Resolves WHICH model(s) write the candidate, and builds the ask.
|
|
182
182
|
#
|
|
183
|
-
# refinement.proposers on the agent (
|
|
184
|
-
# -> refinement.proposer ("deepseek/deepseek-
|
|
183
|
+
# refinement.proposers on the agent (a PANEL)
|
|
184
|
+
# -> refinement.proposer ("deepseek/deepseek-v4-flash" | "deepseek-v4-flash")
|
|
185
185
|
# -> the platform utility_model
|
|
186
186
|
# -> nothing, and the caller refuses. There is no default model here on purpose:
|
|
187
187
|
# guessing one spends an operator's provider budget without being asked.
|
|
@@ -189,17 +189,17 @@ module Insika
|
|
|
189
189
|
module_function
|
|
190
190
|
|
|
191
191
|
# config: the agent's `refinement` hash. -> Proposer | nil (the FIRST of the
|
|
192
|
-
# panel —
|
|
192
|
+
# panel — single-proposer entry point, kept because a deployment that
|
|
193
193
|
# never configured a panel is a panel of one).
|
|
194
194
|
def build(config, utility_model: nil, ask_factory: nil, llm: nil)
|
|
195
195
|
panel(config, utility_model: utility_model, ask_factory: ask_factory, llm: llm).first
|
|
196
196
|
end
|
|
197
197
|
|
|
198
198
|
# -> [Proposer], in configured order, DEDUPED by model ref and capped at the
|
|
199
|
-
#
|
|
199
|
+
# fan-out (says the panel reuses it). Two entries naming the same
|
|
200
200
|
# model are one proposer: asking the same model twice at temperature 0 measures
|
|
201
|
-
# its variance, which is exactly what
|
|
202
|
-
# `llm
|
|
201
|
+
# its variance, which is exactly what rejected for the judges.
|
|
202
|
+
# `llm`: the graph's own RubyLLM context; nil = the global
|
|
203
203
|
# constant. Today only the deployment root builds a panel, and a deployment
|
|
204
204
|
# is one graph per process — the seam exists so an embedded graph that ever
|
|
205
205
|
# gains the refinement commands proposes on its own credentials.
|
|
@@ -213,8 +213,8 @@ module Insika
|
|
|
213
213
|
end
|
|
214
214
|
end
|
|
215
215
|
|
|
216
|
-
# `proposers` accepts either syntax — a bare ref ("deepseek/deepseek-
|
|
217
|
-
# the
|
|
216
|
+
# `proposers` accepts either syntax — a bare ref ("deepseek/deepseek-v4-flash") or
|
|
217
|
+
# the hash form `{ "model" =>, "provider"? => }` — because the two already coexist in
|
|
218
218
|
# this config (`proposer` is a bare ref, `judges` are hashes) and refusing one of
|
|
219
219
|
# them would only teach operators which page they were reading.
|
|
220
220
|
def refs_for(config, utility_model)
|
|
@@ -248,7 +248,7 @@ module Insika
|
|
|
248
248
|
# is actually configured.
|
|
249
249
|
#
|
|
250
250
|
# Returns the MESSAGE, not `.content`: the token counts ride on it and the
|
|
251
|
-
# budget
|
|
251
|
+
# budget is what spends them. `Proposer` reads either shape.
|
|
252
252
|
def ruby_llm_ask(model, provider, llm: nil)
|
|
253
253
|
require "ruby_llm"
|
|
254
254
|
llm ||= RubyLLM
|
|
@@ -4,7 +4,7 @@ require "securerandom"
|
|
|
4
4
|
require "time"
|
|
5
5
|
|
|
6
6
|
module Insika
|
|
7
|
-
# REFINEMENT DOMAIN store
|
|
7
|
+
# REFINEMENT DOMAIN store. One record per refinement RUN:
|
|
8
8
|
# the window that was read, the ranked findings the EvidenceCollector produced,
|
|
9
9
|
# and the run's outcome. RUNTIME data (it is derived from sessions/tasks/traces),
|
|
10
10
|
# so it takes the raw `store:` like SessionStore/TaskStore — not the ConfigStore.
|
|
@@ -15,8 +15,8 @@ module Insika
|
|
|
15
15
|
# Store contract orders lexicographically) and `latest_for` is its last element.
|
|
16
16
|
# An agent id containing ":" would break that split, so it is rejected on write.
|
|
17
17
|
#
|
|
18
|
-
#
|
|
19
|
-
# status was terminal.
|
|
18
|
+
# wrote no edits anywhere — a Run was a REPORT and every non-collecting
|
|
19
|
+
# status was terminal. adds the rest of the lifecycle on
|
|
20
20
|
# the SAME record, additively: a gated candidate and the operator's decision.
|
|
21
21
|
#
|
|
22
22
|
# collecting ─▶ completed ─▶ gating ─▶ awaiting_approval ─▶ applied
|
|
@@ -24,13 +24,13 @@ module Insika
|
|
|
24
24
|
# ╰─▶ failed or the operator did)
|
|
25
25
|
#
|
|
26
26
|
# **The approval lives here, not in `PendingActionStore`** — a deliberate deviation
|
|
27
|
-
# from
|
|
27
|
+
# from That store is coupled to a suspended TURN: `ApproveAction` resolves
|
|
28
28
|
# the record and then calls `executor.approve(task_id)` to wake a fiber. A
|
|
29
29
|
# refinement proposal has no turn and no fiber, so reusing it would mean inventing
|
|
30
30
|
# a task id, a fake `tool` name, and a wake-up that must never do anything — and
|
|
31
31
|
# the operator's approvals inbox would fill with rows that are not tool calls. The
|
|
32
|
-
# property
|
|
33
|
-
# has had it since
|
|
32
|
+
# property actually wanted is durability across a `kill -9`, and this store
|
|
33
|
+
# has had it since.
|
|
34
34
|
class RefinementStore
|
|
35
35
|
include Coercion
|
|
36
36
|
|
|
@@ -42,7 +42,7 @@ module Insika
|
|
|
42
42
|
|
|
43
43
|
# OPEN means "this run will change without anyone asking": work is in flight
|
|
44
44
|
# (`collecting`, `gating`) or a human owes it an answer (`awaiting_approval`).
|
|
45
|
-
# Everything else is terminal — INCLUDING `completed`, which
|
|
45
|
+
# Everything else is terminal — INCLUDING `completed`, which already
|
|
46
46
|
# treated that way and which stays true: a report is finished, and gating one is
|
|
47
47
|
# a new deliberate action, not a continuation. (Widening `terminal?` here is what
|
|
48
48
|
# the Studio's "latest report" lookup reads, so getting it wrong hides the report.)
|
|
@@ -50,7 +50,7 @@ module Insika
|
|
|
50
50
|
|
|
51
51
|
# `candidate`/`gate` are the WINNER — the one proposal a human is asked about, and
|
|
52
52
|
# the only thing `ResolveRefinement` ever applies. `candidates` is the whole panel
|
|
53
|
-
#
|
|
53
|
+
# every candidate that was built, who wrote it, and how
|
|
54
54
|
# it scored, including the ones that lost and the ones the budget never gated. A
|
|
55
55
|
# phase-C run recorded one candidate and no panel; it still reads back correctly,
|
|
56
56
|
# because a panel of one is the same shape.
|
|
@@ -118,7 +118,7 @@ module Insika
|
|
|
118
118
|
end
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
-
#
|
|
121
|
+
# the proposal's lifecycle -------------------------------
|
|
122
122
|
|
|
123
123
|
# Attaches the candidate(s) under gate and moves the run to :gating. Only a
|
|
124
124
|
# `completed` run can be gated: a report with no findings has nothing to propose
|
|
@@ -145,15 +145,15 @@ module Insika
|
|
|
145
145
|
end
|
|
146
146
|
|
|
147
147
|
# Records the gate's verdict. A PASS parks the run at :awaiting_approval — a
|
|
148
|
-
# human still has to say yes, which is the product and not a formality
|
|
148
|
+
# human still has to say yes, which is the product and not a formality. A
|
|
149
149
|
# FAIL is terminal as :rejected, with the gate report as the stated reason: the
|
|
150
150
|
# same finding must re-surface with new evidence before anything is proposed
|
|
151
|
-
# again, so there is no silent retry loop
|
|
151
|
+
# again, so there is no silent retry loop.
|
|
152
152
|
#
|
|
153
153
|
# `report` is the WINNER's (or, when nothing survived, the most informative
|
|
154
154
|
# refusal). `panel` is every scored entry — the store attaches it as-is and picks
|
|
155
155
|
# the winning candidate out of it by id. Which candidate WON is the caller's
|
|
156
|
-
# ranking decision
|
|
156
|
+
# ranking decision; this store does not rank, it records. -> Run.
|
|
157
157
|
def gated(id, report:, panel: nil, cost: nil)
|
|
158
158
|
update(id) do |record|
|
|
159
159
|
unless record["status"] == "gating"
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
# The reliability policy for the turn's provider interaction (WS3): retries
|
|
5
|
+
# with exponential backoff, mid-turn ROTATION across the fallback chain, and
|
|
6
|
+
# the circuit breaker — all as DATA on `AgentProfile#reliability`, never a
|
|
7
|
+
# parallel code path. The failure classification is B9's
|
|
8
|
+
# (ProviderErrorClassifier): ONLY :retryable / :rate_limited_* ever retry or
|
|
9
|
+
# rotate; a :fatal is re-raised immediately (a poisoned credential must not
|
|
10
|
+
# hammer N models).
|
|
11
|
+
#
|
|
12
|
+
# Each ATTEMPT is a fresh chat built by the caller (the coordinator yields the
|
|
13
|
+
# selection): a failed `ask` leaves its user message in the chat, so re-asking
|
|
14
|
+
# the same chat would double the input. The customer-visible answer comes only
|
|
15
|
+
# from the attempt that returns; the fragments of failed attempts ride
|
|
16
|
+
# :intermediate (operator-side) and die with the turn.
|
|
17
|
+
#
|
|
18
|
+
# The breaker is per (tenant, provider/model): a node whose circuit is open is
|
|
19
|
+
# skipped (fail-fast with CircuitOpenError when the PRIMARY is open — the turn
|
|
20
|
+
# dies in ms, no provider call); a node that tripped mid-turn moves on.
|
|
21
|
+
class Reliability
|
|
22
|
+
DEFAULT_TIMEOUT = 30 # seconds per attempt (reliability["timeout"])
|
|
23
|
+
|
|
24
|
+
# circuit_store: CircuitState (the breaker's durable cells).
|
|
25
|
+
# event_stream: where the reliability events go (:provider_fallback, ...).
|
|
26
|
+
# sleeper: ->(seconds) — the backoff wait; injectable for specs.
|
|
27
|
+
def initialize(circuit_store:, event_stream:, sleeper: nil)
|
|
28
|
+
@circuit_store = circuit_store
|
|
29
|
+
@event_stream = event_stream
|
|
30
|
+
@sleeper = sleeper || method(:backoff_wait)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# policy: the profile's reliability data (string keys) — the caller skips
|
|
34
|
+
# this coordinator entirely when nil (parity).
|
|
35
|
+
# tenant: the command tenant (breaker scoping; nil = platform).
|
|
36
|
+
# agent: the agent id (event attribution — WS6 alerts read it).
|
|
37
|
+
# selection: the resolved primary ModelSelection.
|
|
38
|
+
# chain: [{ model:, provider: }] fallback candidates (profile's first,
|
|
39
|
+
# then the platform's resolved fallbacks).
|
|
40
|
+
# attempt: ->(selection, attempt_index) { response } — build the chat for
|
|
41
|
+
# that selection and ask. May RAISE a provider-family error.
|
|
42
|
+
#
|
|
43
|
+
# -> the successful response. Raises CircuitOpenError (primary open),
|
|
44
|
+
# or the last retryable error when every node exhausted its retries.
|
|
45
|
+
# NOTHING about a run is stored on `self`: one Reliability instance serves
|
|
46
|
+
# every concurrent turn, and `ask` is a suspension point — an ivar written
|
|
47
|
+
# here would be read back after another fiber's turn overwrote it, and the
|
|
48
|
+
# WS6 alert would name the wrong agent (and, through it, the wrong tenant).
|
|
49
|
+
# The run's identity rides the stack.
|
|
50
|
+
def call(policy:, tenant:, agent: nil, selection:, chain:, &attempt)
|
|
51
|
+
nodes = ([selection] + Array(chain)).map { |node| { selection: node, tries: 0 } }
|
|
52
|
+
retries = [policy["retries"].to_i, 0].max
|
|
53
|
+
breaker = breaker_config(policy)
|
|
54
|
+
# The per-attempt ceiling. A policy WITHOUT a timeout is DEFAULT_TIMEOUT —
|
|
55
|
+
# nothing config overrides here (the old [.., 1].max silently made every
|
|
56
|
+
# unset profile die in ~1s, WS3).
|
|
57
|
+
configured_timeout = policy["timeout"].to_i
|
|
58
|
+
timeout = configured_timeout.positive? ? configured_timeout : DEFAULT_TIMEOUT
|
|
59
|
+
# declaraed HERE (not inside a block) so the post-loop `raise` sees the
|
|
60
|
+
# method-local binding — a first assignment inside a block would not leak.
|
|
61
|
+
last_error = nil
|
|
62
|
+
|
|
63
|
+
# Fail-fast BEFORE any provider call: the PRIMARY's circuit open means
|
|
64
|
+
# this provider family is known-dead — the turn dies in ms.
|
|
65
|
+
if breaker && breaker_open?(tenant, selection, breaker)
|
|
66
|
+
raise circuit_open(tenant, selection, breaker)
|
|
67
|
+
end
|
|
68
|
+
# the last node we actually ASKED: the `from` of a rotation. A node the
|
|
69
|
+
# breaker skipped was never asked, so it is never the `from`.
|
|
70
|
+
asked = nil
|
|
71
|
+
nodes.each do |node|
|
|
72
|
+
selection = node[:selection]
|
|
73
|
+
next if breaker && breaker_open?(tenant, selection, breaker)
|
|
74
|
+
|
|
75
|
+
# ROTATION is an EVENT, not an inference from the usage attribution
|
|
76
|
+
# (WS3): the trace names the node we left, the node we moved to and why.
|
|
77
|
+
# Emitted for every node past the first one asked — with or WITHOUT a
|
|
78
|
+
# breaker (a fallback policy with no circuit_breaker used to rotate in
|
|
79
|
+
# complete silence).
|
|
80
|
+
emit_fallback(agent, asked, selection, last_error) if asked
|
|
81
|
+
asked = selection
|
|
82
|
+
|
|
83
|
+
attempts = retries + 1
|
|
84
|
+
attempts.times do |index|
|
|
85
|
+
node[:tries] += 1
|
|
86
|
+
begin
|
|
87
|
+
response = with_attempt_timeout(timeout) { yield selection, node[:tries] }
|
|
88
|
+
@circuit_store.record_success(tenant: tenant, ref: ref_of(selection)) if breaker
|
|
89
|
+
return response
|
|
90
|
+
rescue StandardError => e
|
|
91
|
+
last_error = e
|
|
92
|
+
# A :fatal provider error — or ANYTHING that is not a provider
|
|
93
|
+
# failure at all (a bug, a domain error, a guardrail raise) — is
|
|
94
|
+
# never retried, never rotated (B9's structural rule). Only
|
|
95
|
+
# retryable/rate-limited (and the per-attempt timeout we raised)
|
|
96
|
+
# spend the retry budget. The B9 classifier is class-name based, so
|
|
97
|
+
# OUR TimeoutError reads as :fatal — the retryable_failure? check
|
|
98
|
+
# (which owns the reliability-stage timeout) must decide FIRST, or
|
|
99
|
+
# the :fatal guard would swallow it (WS3: a timeout never retried,
|
|
100
|
+
# never rotated).
|
|
101
|
+
retryable = retryable_failure?(e)
|
|
102
|
+
raise unless retryable
|
|
103
|
+
raise if kind_of(e) == :fatal && !e.is_a?(Insika::TimeoutError)
|
|
104
|
+
|
|
105
|
+
record_failure(tenant, selection, breaker, e, agent)
|
|
106
|
+
# the last attempt of the last node re-raises; otherwise back off
|
|
107
|
+
# and give the next attempt/node a turn.
|
|
108
|
+
if index < attempts - 1 || node != nodes.last
|
|
109
|
+
@sleeper.call(backoff_seconds(policy, index))
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
raise last_error if last_error
|
|
115
|
+
|
|
116
|
+
raise Insika::Error, "reliability loop exhausted without a result"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def breaker_config(policy)
|
|
122
|
+
b = policy["circuit_breaker"]
|
|
123
|
+
return nil unless b.is_a?(Hash) && b["after"].to_i.positive?
|
|
124
|
+
|
|
125
|
+
{ after: b["after"].to_i, within: b["within"].to_i, cooldown: b["cooldown"].to_i }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The breaker cell reads need the POLICY's numbers; they ride as args. Only
|
|
129
|
+
# :open FAIL-FASTS; :half_open (cooldown elapsed) is the TRIAL — allowed.
|
|
130
|
+
def breaker_open?(tenant, selection, breaker)
|
|
131
|
+
@circuit_store.state(tenant: tenant, ref: ref_of(selection),
|
|
132
|
+
after: breaker[:after], within: breaker[:within],
|
|
133
|
+
cooldown: breaker[:cooldown]) == :open
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def circuit_open(tenant, selection, breaker)
|
|
137
|
+
Insika::CircuitOpenError.new(
|
|
138
|
+
"circuit open for #{ref_of(selection)}",
|
|
139
|
+
ref: ref_of(selection),
|
|
140
|
+
retry_after: @circuit_store.retry_after(tenant: tenant, ref: ref_of(selection),
|
|
141
|
+
cooldown: breaker[:cooldown])
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The failure is ALWAYS an event; only the circuit bump needs a breaker (the
|
|
146
|
+
# old `return unless breaker` made a retry/fallback policy without a
|
|
147
|
+
# circuit_breaker run with no trace at all).
|
|
148
|
+
def record_failure(tenant, selection, breaker, error, agent)
|
|
149
|
+
emit(:provider_failure,
|
|
150
|
+
{ agent: agent, ref: ref_of(selection), error: error.class.name, kind: kind_of(error) })
|
|
151
|
+
return unless breaker
|
|
152
|
+
|
|
153
|
+
tripped = @circuit_store.record_failure(
|
|
154
|
+
tenant: tenant, ref: ref_of(selection),
|
|
155
|
+
after: breaker[:after], within: breaker[:within]
|
|
156
|
+
)
|
|
157
|
+
# the failure that TRIPPED the circuit is itself an alert (WS6).
|
|
158
|
+
emit(:breaker_open, { agent: agent, ref: ref_of(selection), tenant: tenant }) if tripped == :open
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The mid-turn rotation across the fallback chain: the node we left, the one
|
|
162
|
+
# we moved to, and the error that spent the previous node's retries.
|
|
163
|
+
def emit_fallback(agent, from, to, error)
|
|
164
|
+
emit(:provider_fallback,
|
|
165
|
+
{ agent: agent, from: ref_of(from), to: ref_of(to),
|
|
166
|
+
error: error&.class&.name, kind: error && kind_of(error) })
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def kind_of(error) = ProviderErrorClassifier.classify(error).kind
|
|
170
|
+
|
|
171
|
+
# A provider-family error OR the per-attempt timeout: both are transient
|
|
172
|
+
# transport-class failures that spend the retry budget.
|
|
173
|
+
def retryable_failure?(error)
|
|
174
|
+
ProviderErrorClassifier.provider_error?(error) ||
|
|
175
|
+
(error.is_a?(Insika::TimeoutError) && error.stage.to_s == "reliability")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def with_attempt_timeout(timeout, &blk)
|
|
179
|
+
return yield unless Async::Task.current?
|
|
180
|
+
Async::Task.current.with_timeout(timeout) { yield }
|
|
181
|
+
rescue Async::TimeoutError
|
|
182
|
+
# a per-attempt timeout is a TRANSPORT-class failure: counted, retried.
|
|
183
|
+
raise Insika::TimeoutError.new("provider attempt exceeded #{timeout}s", stage: :reliability)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# The breaker cell id: "provider/model" for the ref'd node — a ModelSelection
|
|
187
|
+
# (primary) or a { model:, provider: } hash (fallback node).
|
|
188
|
+
def ref_of(selection)
|
|
189
|
+
model = selection.respond_to?(:model) ? selection.model.to_s : selection[:model].to_s
|
|
190
|
+
provider = selection.respond_to?(:provider) ? selection.provider : selection[:provider]
|
|
191
|
+
provider ? "#{provider}/#{model}" : model
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def backoff_seconds(policy, index)
|
|
195
|
+
case policy["backoff"].to_s
|
|
196
|
+
when "exponential" then 2**index
|
|
197
|
+
else index + 1
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def backoff_wait(seconds)
|
|
202
|
+
Async::Task.current? ? Async::Task.current.sleep(seconds) : Kernel.sleep(seconds)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def emit(type, data)
|
|
206
|
+
@event_stream.emit(Insika::Event.new(
|
|
207
|
+
type: type, data: data, meta: { at: Time.now.utc.iso8601 }
|
|
208
|
+
))
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|