insika 0.3.0 → 0.8.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 +296 -0
- data/README.md +48 -12
- data/bin/insika +725 -0
- data/bin/insika-router +87 -0
- data/docs/AGENTS.md +116 -406
- data/docs/API.md +5 -5
- data/docs/ARCHITECTURE.md +3 -2
- data/docs/ARTIFACTS.md +137 -0
- data/docs/BENCHMARK.md +2 -2
- data/docs/CHANNELS.md +14 -14
- data/docs/CONTEXT.md +63 -19
- data/docs/DEMO.md +80 -0
- data/docs/DEPLOY.md +87 -10
- data/docs/EMBEDDING.md +1 -1
- data/docs/EVALS.md +128 -3
- data/docs/FACTS.md +3 -3
- data/docs/HARVEST.md +5 -6
- data/docs/KNOWLEDGE.md +290 -0
- data/docs/LOADTEST.md +17 -29
- data/docs/MEDIA.md +128 -0
- data/docs/OBSERVABILITY.md +46 -12
- data/docs/OUTCOMES.md +137 -0
- data/docs/PLUGINS.md +51 -6
- data/docs/POLICY.md +222 -0
- data/docs/REFINEMENT.md +14 -9
- data/docs/RELEASING.md +4 -4
- data/docs/ROUTER.md +213 -0
- data/docs/RUNNING-LOCAL.md +5 -5
- data/docs/SCHEDULING.md +121 -0
- data/docs/SECURITY.md +23 -7
- data/docs/SKILLS.md +11 -2
- data/docs/SOAK.md +3 -3
- data/docs/TEMPLATES.md +134 -0
- data/docs/TOOLS.md +176 -27
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/_includes/head_custom.html +5 -0
- data/docs/_includes/title.html +13 -0
- data/docs/_sass/color_schemes/insika.scss +32 -0
- data/docs/_sass/custom/custom.scss +199 -0
- data/docs/_sass/custom/setup.scss +26 -0
- data/docs/assets/img/favicon.svg +7 -0
- data/docs/assets/img/insika-mark.svg +7 -0
- data/docs/core-concepts.md +21 -0
- data/docs/domain.md +4 -4
- data/docs/improve.md +20 -0
- data/docs/index.md +8 -5
- data/docs/integrate.md +20 -0
- data/docs/operate.md +13 -6
- data/docs/prompts/ADD-TOOL.md +118 -0
- data/docs/prompts/DIAGNOSE-TURN.md +65 -0
- data/docs/prompts/GO-LIVE.md +138 -0
- data/docs/prompts/RUN-EXAMPLES.md +70 -0
- data/docs/reference.md +19 -0
- data/docs/ship.md +10 -2
- data/docs/start-here.md +18 -0
- data/lib/insika/agent_profile.rb +99 -17
- data/lib/insika/artifact_signing.rb +82 -0
- data/lib/insika/artifact_store.rb +160 -0
- data/lib/insika/channel_delivery.rb +1 -1
- data/lib/insika/chat_builder.rb +50 -19
- data/lib/insika/commands/agent_payload.rb +2 -2
- data/lib/insika/commands/backfill_knowledge.rb +145 -0
- data/lib/insika/commands/delete_artifact.rb +35 -0
- data/lib/insika/commands/delete_concept.rb +34 -0
- data/lib/insika/commands/delete_mcp.rb +6 -2
- data/lib/insika/commands/delete_tenant_data.rb +15 -3
- data/lib/insika/commands/gate_refinement.rb +1 -1
- data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
- data/lib/insika/commands/restore_concept.rb +34 -0
- data/lib/insika/commands/seed_demo_data.rb +31 -0
- data/lib/insika/commands/upsert_mcp.rb +6 -3
- data/lib/insika/commands/write_concept.rb +57 -0
- data/lib/insika/compaction.rb +196 -0
- data/lib/insika/context/builder.rb +6 -2
- data/lib/insika/context/fragment.rb +4 -1
- data/lib/insika/context/priority.rb +8 -0
- data/lib/insika/context/providers/briefing.rb +53 -24
- data/lib/insika/context/providers/knowledge.rb +108 -0
- data/lib/insika/context/providers/prompt.rb +30 -24
- data/lib/insika/context/providers/session.rb +46 -10
- data/lib/insika/context_trace_store.rb +11 -1
- data/lib/insika/cron.rb +189 -0
- data/lib/insika/demo/agent_attrs.rb +43 -0
- data/lib/insika/demo/golden_cases.rb +81 -0
- data/lib/insika/demo/seeder.rb +336 -0
- data/lib/insika/doctor.rb +280 -17
- data/lib/insika/dsl/definition.rb +3 -2
- data/lib/insika/dsl/runtime.rb +64 -79
- data/lib/insika/dsl/server_boot.rb +23 -1
- data/lib/insika/dsl/system.rb +10 -2
- data/lib/insika/dsl.rb +103 -2
- data/lib/insika/env_schema.rb +21 -7
- data/lib/insika/evals/golden.rb +41 -4
- data/lib/insika/evals/judge.rb +47 -2
- data/lib/insika/evals/pairwise.rb +11 -0
- data/lib/insika/evals/persona.rb +98 -0
- data/lib/insika/evals/runner.rb +9 -0
- data/lib/insika/evals/simulator.rb +225 -0
- data/lib/insika/evals/transport.rb +84 -2
- data/lib/insika/event_stream.rb +10 -0
- data/lib/insika/executor.rb +295 -55
- data/lib/insika/followup_policy.rb +2 -25
- data/lib/insika/golden_store.rb +16 -1
- data/lib/insika/grounding/matcher.rb +1 -1
- data/lib/insika/knowledge.rb +680 -0
- data/lib/insika/knowledge_store.rb +140 -0
- data/lib/insika/loop_detector.rb +5 -34
- data/lib/insika/mcp_client.rb +94 -0
- data/lib/insika/mcp_json.rb +74 -0
- data/lib/insika/mcp_live_tool.rb +43 -0
- data/lib/insika/mcp_store.rb +98 -26
- data/lib/insika/mcp_tool_ingestor.rb +30 -8
- data/lib/insika/mcp_tool_registry.rb +100 -0
- data/lib/insika/media.rb +115 -31
- data/lib/insika/message_origin.rb +1 -1
- data/lib/insika/middleware.rb +9 -0
- data/lib/insika/onboarding.rb +17 -1
- data/lib/insika/outcome_store.rb +1 -1
- data/lib/insika/overlay_tool_registry.rb +37 -17
- data/lib/insika/packaging.rb +2 -2
- data/lib/insika/profile_source.rb +15 -1
- data/lib/insika/prompt_catalog.rb +10 -0
- data/lib/insika/retention.rb +36 -1
- data/lib/insika/router/app.rb +157 -0
- data/lib/insika/router/backend_pool.rb +98 -0
- data/lib/insika/router/hash_ring.rb +55 -0
- data/lib/insika/router/proxy_body.rb +34 -0
- data/lib/insika/router/session_key.rb +54 -0
- data/lib/insika/router.rb +18 -0
- data/lib/insika/schedule.rb +177 -0
- data/lib/insika/schedule_engine.rb +314 -0
- data/lib/insika/schedule_store.rb +208 -0
- data/lib/insika/server/app.rb +105 -15
- data/lib/insika/server/rack_app.rb +5 -1
- data/lib/insika/server/responses.rb +5 -5
- data/lib/insika/session_store.rb +34 -4
- data/lib/insika/settings_store.rb +8 -1
- data/lib/insika/skill_catalog.rb +12 -0
- data/lib/insika/soak/runner.rb +4 -4
- data/lib/insika/steer_injector.rb +21 -10
- data/lib/insika/studio/app.rb +591 -47
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +21 -21
- data/lib/insika/studio/forms.rb +57 -5
- data/lib/insika/studio/nav_icons.rb +14 -1
- data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
- data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
- data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
- data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
- data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
- data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
- data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
- data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
- data/lib/insika/studio/views/_agents_master.erb +44 -0
- data/lib/insika/studio/views/_message.erb +49 -32
- data/lib/insika/studio/views/agent_detail.erb +61 -820
- data/lib/insika/studio/views/agents.erb +70 -57
- data/lib/insika/studio/views/artifact.erb +23 -0
- data/lib/insika/studio/views/artifacts.erb +59 -0
- data/lib/insika/studio/views/evals.erb +2 -2
- data/lib/insika/studio/views/facts.erb +1 -1
- data/lib/insika/studio/views/funnel.erb +1 -1
- data/lib/insika/studio/views/home.erb +106 -67
- data/lib/insika/studio/views/knowledge.erb +123 -0
- data/lib/insika/studio/views/layout.erb +14 -11
- data/lib/insika/studio/views/mcp.erb +174 -80
- data/lib/insika/studio/views/session.erb +231 -177
- data/lib/insika/studio/views/settings.erb +50 -1
- data/lib/insika/studio/views/skills.erb +1 -1
- data/lib/insika/studio/views/tools.erb +24 -9
- data/lib/insika/telemetry/recorder.rb +49 -1
- data/lib/insika/templates/browser-agent/README.md +36 -0
- data/lib/insika/templates/browser-agent/agent.rb +49 -0
- data/lib/insika/templates/daily-digest/README.md +47 -0
- data/lib/insika/templates/daily-digest/agent.rb +77 -0
- data/lib/insika/templates/repo-explorer/README.md +36 -0
- data/lib/insika/templates/repo-explorer/agent.rb +45 -0
- data/lib/insika/templates/research-analyst/README.md +26 -0
- data/lib/insika/templates/research-analyst/agent.rb +68 -0
- data/lib/insika/templates/review-panel/README.md +20 -0
- data/lib/insika/templates/review-panel/agent.rb +50 -0
- data/lib/insika/templates/travel-planner/README.md +35 -0
- data/lib/insika/templates/travel-planner/agent.rb +87 -0
- data/lib/insika/templates.rb +112 -0
- data/lib/insika/tick.rb +24 -12
- data/lib/insika/timezone.rb +45 -0
- data/lib/insika/tool_batch.rb +67 -0
- data/lib/insika/tool_usage_report.rb +162 -0
- data/lib/insika/tools/generate_image.rb +52 -7
- data/lib/insika/tools/load_knowledge.rb +74 -0
- data/lib/insika/tools/run_persona_eval.rb +328 -0
- data/lib/insika/tools/save_artifact.rb +95 -0
- data/lib/insika/turn_budget.rb +91 -0
- data/lib/insika/turn_output.rb +1 -1
- data/lib/insika/turn_state.rb +15 -4
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +184 -12
- data/lib/insika/wiring/graph_chat.rb +102 -0
- data/lib/insika.rb +64 -0
- metadata +109 -5
- data/docs/build.md +0 -14
- data/docs/understand.md +0 -10
data/lib/insika/doctor.rb
CHANGED
|
@@ -104,7 +104,8 @@ module Insika
|
|
|
104
104
|
shadow_pair_store: nil, soak_envelope_path: nil, context_providers: nil,
|
|
105
105
|
memory_store: nil, agent_ids: nil, funnel_store: nil, outcome_store: nil,
|
|
106
106
|
followup_store: nil, contact_store: nil, proposal_store: nil,
|
|
107
|
-
|
|
107
|
+
schedule_store: nil,
|
|
108
|
+
harvest_store: nil, harvest_criterion: nil, mcp_store: nil)
|
|
108
109
|
@env = env
|
|
109
110
|
@settings_store = settings_store
|
|
110
111
|
@llm_provider_store = llm_provider_store
|
|
@@ -136,10 +137,16 @@ module Insika
|
|
|
136
137
|
# the distillation check — nil collaborator = the check
|
|
137
138
|
# reports declarations only (counts skipped).
|
|
138
139
|
@proposal_store = proposal_store
|
|
140
|
+
# the schedules check — nil collaborator = the check
|
|
141
|
+
# reports declarations only (skip counts skipped).
|
|
142
|
+
@schedule_store = schedule_store
|
|
139
143
|
# the harvest check — nil collaborators = the check
|
|
140
144
|
# reports declarations only (counts skipped).
|
|
141
145
|
@harvest_store = harvest_store
|
|
142
146
|
@harvest_criterion = harvest_criterion
|
|
147
|
+
# the mcp check — nil collaborator = the check reports nothing
|
|
148
|
+
# (env-only callers stay cheap).
|
|
149
|
+
@mcp_store = mcp_store
|
|
143
150
|
end
|
|
144
151
|
|
|
145
152
|
# -> Report. Never raises (a broken check degrades to an :error Finding).
|
|
@@ -168,11 +175,12 @@ module Insika
|
|
|
168
175
|
private
|
|
169
176
|
|
|
170
177
|
def checks = %i[check_env check_settings_schema check_default_model check_db check_llm_provider
|
|
171
|
-
check_admin_token check_data_tools check_prompt_files
|
|
172
|
-
check_web_widget check_skill_eager check_skill_drift check_shadow_parity
|
|
178
|
+
check_admin_token check_web_concurrency check_data_tools check_mcp check_prompt_files
|
|
179
|
+
check_relay_channel check_web_widget check_skill_eager check_skill_drift check_shadow_parity
|
|
173
180
|
check_soak_envelope check_turn_timing check_grounding check_cache_layers
|
|
174
181
|
check_memory_scopes check_funnel_declarations check_followup check_distill
|
|
175
|
-
check_harvest check_guardrail_corpora
|
|
182
|
+
check_compaction check_harvest check_schedules check_guardrail_corpora
|
|
183
|
+
check_tool_allowlist_policy]
|
|
176
184
|
|
|
177
185
|
def safe(check)
|
|
178
186
|
Array(send(check))
|
|
@@ -336,6 +344,54 @@ module Insika
|
|
|
336
344
|
message: "ADMIN_TOKEN unset — /studio is fail-closed (login denied) and the gateway has no fallback token", fix: nil)]
|
|
337
345
|
end
|
|
338
346
|
|
|
347
|
+
# WEB_CONCURRENCY>1 is a legitimate config (docs/DEPLOY.md "The process
|
|
348
|
+
# model") — but it silently drops per-worker session guarantees
|
|
349
|
+
# (FIFO/collect/steer) unless the operator has sticky routing per session in
|
|
350
|
+
# front, and nothing else in the deployment surfaces that drift. This is
|
|
351
|
+
# exactly how a staging service ended up at N=4 unnoticed and leaked a reply
|
|
352
|
+
# across sessions. The engine cannot see whether sticky routing exists off
|
|
353
|
+
# Railway, so that case only warns — N>1 stays a valid choice for an
|
|
354
|
+
# operator who put a real sticky proxy in front. On Railway it is not a
|
|
355
|
+
# valid choice TODAY: Railway's own docs say it "does not support sticky
|
|
356
|
+
# sessions" and randomly distributes traffic — there is no way to satisfy
|
|
357
|
+
# the precondition on this platform, so this errors instead of warning.
|
|
358
|
+
# RAILWAY_ENVIRONMENT_NAME is injected into every Railway deployment
|
|
359
|
+
# (docs.railway.com/variables/reference), so its presence is a reliable
|
|
360
|
+
# "are we on Railway" signal.
|
|
361
|
+
def check_web_concurrency
|
|
362
|
+
n = @env["WEB_CONCURRENCY"].to_i
|
|
363
|
+
return [ok("web-concurrency", "WEB_CONCURRENCY=1 (default) — session semantics hold cluster-wide")] if n <= 1
|
|
364
|
+
|
|
365
|
+
if router_detected?
|
|
366
|
+
return [ok("web-concurrency", "WEB_CONCURRENCY=#{n} behind a detected insika-router " \
|
|
367
|
+
"sticky routing satisfies the precondition")]
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
if Insika::Coercion.present?(@env["RAILWAY_ENVIRONMENT_NAME"])
|
|
371
|
+
return [Finding.new(check: "web-concurrency", severity: :error,
|
|
372
|
+
message: "WEB_CONCURRENCY=#{n} on Railway — Railway does not support sticky " \
|
|
373
|
+
"sessions (docs.railway.com/deployments/scaling), so per-session " \
|
|
374
|
+
"FIFO/collect/steer WILL break across workers with no way to fix it " \
|
|
375
|
+
"at the routing layer; set WEB_CONCURRENCY=1, or run insika-router " \
|
|
376
|
+
"in front of N local workers instead", fix: nil)]
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
[Finding.new(check: "web-concurrency", severity: :warn,
|
|
380
|
+
message: "WEB_CONCURRENCY=#{n} — per-session FIFO/collect/steer only hold on the worker " \
|
|
381
|
+
"that owns the session; make sure sticky routing per session sits in front (e.g. " \
|
|
382
|
+
"insika-router, set INSIKA_ROUTER_BACKENDS/_DNS), or accept " \
|
|
383
|
+
"per-worker best-effort (docs/DEPLOY.md \"The process model\")", fix: nil)]
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# A router is "detected", not verified — the doctor cannot see whether a
|
|
387
|
+
# process at these addresses actually runs `insika-router`, only that the
|
|
388
|
+
# operator configured one. Same precedent as every other
|
|
389
|
+
# env-based capability check in this file.
|
|
390
|
+
def router_detected?
|
|
391
|
+
Insika::Coercion.present?(@env["INSIKA_ROUTER_BACKENDS"]) ||
|
|
392
|
+
Insika::Coercion.present?(@env["INSIKA_ROUTER_BACKENDS_DNS"])
|
|
393
|
+
end
|
|
394
|
+
|
|
339
395
|
# the soak envelope (a deployment-side file). Absent is :info — not every
|
|
340
396
|
# deployment soaks, and the tooling is optional. Present-and-broken is
|
|
341
397
|
# :error — an envelope that exists but does not parse is a pre-declaration
|
|
@@ -667,26 +723,59 @@ module Insika
|
|
|
667
723
|
# the mangled prompt on every turn, and nothing else would ever say so: the file is
|
|
668
724
|
# present, non-empty, and the agent answers — worse than a crash. Found on the pilot
|
|
669
725
|
# by an `insika refine` report, three weeks after the fact.
|
|
726
|
+
#
|
|
727
|
+
# The same sweep also WARNS (never errors) on a file that outgrew a prompt.
|
|
728
|
+
# Merchant packs are LLM-generated (generate-merchant-pack), and generated prose
|
|
729
|
+
# bloats: the pilot's 28 KB AGENTS.md is the local example, and the ETH Zurich
|
|
730
|
+
# instruction-file study puts the cost of that shape at 20%+ extra tokens per
|
|
731
|
+
# turn for no extra instruction-following. The thresholds are deliberately
|
|
732
|
+
# generous — a hand-written file never meets them; only the generated shape does.
|
|
670
733
|
def check_prompt_files
|
|
671
734
|
return [] unless @agent_file_store
|
|
672
735
|
|
|
673
736
|
agents = @agent_file_store.agents
|
|
674
|
-
wrapped =
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
737
|
+
wrapped = []
|
|
738
|
+
oversized = []
|
|
739
|
+
agents.each do |agent|
|
|
740
|
+
@agent_file_store.list(agent).each do |name|
|
|
741
|
+
content = @agent_file_store.read(agent, name)
|
|
742
|
+
if wrapped_content?(content)
|
|
743
|
+
wrapped << Finding.new(check: "prompt-files", severity: :error, fix: nil,
|
|
744
|
+
message: "agent '#{agent}' file '#{name}' holds a serialized object, not text — " \
|
|
745
|
+
"the model receives `{\"content\" => …}` on one line, escapes and all. " \
|
|
746
|
+
"Recover the markdown from inside the wrapper and write it back.")
|
|
747
|
+
elsif (finding = oversized_prompt(agent, name, content))
|
|
748
|
+
oversized << finding
|
|
749
|
+
end
|
|
682
750
|
end
|
|
683
751
|
end
|
|
684
|
-
return wrapped if wrapped.any?
|
|
752
|
+
return wrapped + oversized if wrapped.any? || oversized.any?
|
|
685
753
|
|
|
686
754
|
total = agents.sum { |a| @agent_file_store.list(a).length }
|
|
687
755
|
[ok("prompt-files", "#{total} prompt file(s) across #{agents.length} agent(s): all text")]
|
|
688
756
|
end
|
|
689
757
|
|
|
758
|
+
# WARN thresholds for one prompt file. ~6 000 estimated tokens (~24 KB) or 600
|
|
759
|
+
# lines: the pilot's generated 28 KB / 292-line AGENTS.md trips the token bar,
|
|
760
|
+
# every hand-written demo file stays far under both. Estimate = chars/4, the
|
|
761
|
+
# same yardstick as Insika::TokenEstimator — cheap and honest about being ±15%.
|
|
762
|
+
PROMPT_FILE_WARN_TOKENS = 6_000
|
|
763
|
+
PROMPT_FILE_WARN_LINES = 600
|
|
764
|
+
|
|
765
|
+
def oversized_prompt(agent, name, content)
|
|
766
|
+
text = content.to_s
|
|
767
|
+
tokens = Insika::TokenEstimator.estimate(text)
|
|
768
|
+
lines = text.lines.count
|
|
769
|
+
return nil if tokens <= PROMPT_FILE_WARN_TOKENS && lines <= PROMPT_FILE_WARN_LINES
|
|
770
|
+
|
|
771
|
+
Finding.new(check: "prompt-files", severity: :warn, fix: nil,
|
|
772
|
+
message: "agent '#{agent}' file '#{name}' is ~#{tokens} tokens over #{lines} line(s) " \
|
|
773
|
+
"(threshold: #{PROMPT_FILE_WARN_TOKENS} tokens / #{PROMPT_FILE_WARN_LINES} lines) — " \
|
|
774
|
+
"a prompt this large costs 20%+ more tokens on every turn for no better " \
|
|
775
|
+
"instruction-following. Trim it, or split the reference material into skills " \
|
|
776
|
+
"the agent loads on demand.")
|
|
777
|
+
end
|
|
778
|
+
|
|
690
779
|
# Cheap and specific: Ruby's inspect of a Hash whose first key is a string. A real
|
|
691
780
|
# prompt does not open with `{"…" =>`.
|
|
692
781
|
def wrapped_content?(content) = /\A\s*\{\s*"[^"]+"\s*=>/.match?(content.to_s)
|
|
@@ -694,11 +783,50 @@ def wrapped_content?(content) = /\A\s*\{\s*"[^"]+"\s*=>/.match?(content.to_s)
|
|
|
694
783
|
def check_data_tools
|
|
695
784
|
return [] unless @tool_store
|
|
696
785
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
786
|
+
raws = @tool_store.all_raw
|
|
787
|
+
broken = raws.filter_map { |raw| broken_tool(raw) }
|
|
788
|
+
legacy = raws.select { |raw| raw["group"].to_s.start_with?("mcp:") }.map { |raw| legacy_mcp_snapshot(raw) }
|
|
789
|
+
base = broken.empty? ? [ok("data-tools", "#{raws.length} data tool(s): every definition valid")] : broken
|
|
790
|
+
base + legacy
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
# A data-tool in an `mcp:*` group was ingested by the
|
|
794
|
+
# RETIRED snapshot path (McpToolIngestor) — the live registry now serves
|
|
795
|
+
# that same MCP instance's tools directly. Still executes fine (never
|
|
796
|
+
# removed automatically), just a duplicate worth cleaning up.
|
|
797
|
+
def legacy_mcp_snapshot(raw)
|
|
798
|
+
Finding.new(check: "data-tools", severity: :info, fix: nil,
|
|
799
|
+
message: "data tool '#{raw["name"]}' (group #{raw["group"]}) is a legacy MCP " \
|
|
800
|
+
"snapshot — that instance's tools now run LIVE via the MCP registry; " \
|
|
801
|
+
"this frozen copy still works but duplicates it, safe to remove.")
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
# An http/sse instance whose credentials still sit under `env`
|
|
805
|
+
# (the pre-RFC meaning) is READ as `headers` (McpStore#raw), but the
|
|
806
|
+
# record on disk is unchanged until the operator re-saves it — flag it so
|
|
807
|
+
# it doesn't linger silently. A stdio instance that is enabled but
|
|
808
|
+
# INSIKA_MCP_STDIO is not set will save fine but refuse to start.
|
|
809
|
+
def check_mcp
|
|
810
|
+
return [] unless @mcp_store
|
|
811
|
+
|
|
812
|
+
raws = @mcp_store.all_raw
|
|
813
|
+
gated = raws.select do |r|
|
|
814
|
+
r["transport"].to_s == "stdio" && r["enabled"] &&
|
|
815
|
+
!Insika::EnvSchema.truthy?(Insika::EnvSchema.read("INSIKA_MCP_STDIO", @env))
|
|
816
|
+
end
|
|
817
|
+
findings = @mcp_store.legacy_header_names.map do |name|
|
|
818
|
+
Finding.new(check: "mcp", severity: :warn, fix: nil,
|
|
819
|
+
message: "MCP instance '#{name}' still stores credentials under 'env' — " \
|
|
820
|
+
"re-save it so they move to 'headers' (env is now stdio-only).")
|
|
821
|
+
end
|
|
822
|
+
findings += gated.map do |r|
|
|
823
|
+
Finding.new(check: "mcp", severity: :info, fix: nil,
|
|
824
|
+
message: "MCP instance '#{r["name"]}' is stdio and enabled, but INSIKA_MCP_STDIO " \
|
|
825
|
+
"is not set — it will refuse to start.")
|
|
826
|
+
end
|
|
827
|
+
return [ok("mcp", "#{raws.length} MCP instance(s): no legacy env-as-headers records")] if findings.empty?
|
|
700
828
|
|
|
701
|
-
|
|
829
|
+
findings
|
|
702
830
|
end
|
|
703
831
|
|
|
704
832
|
# the doctor cannot run a turn, so it cannot prove purity — it
|
|
@@ -774,6 +902,46 @@ def wrapped_content?(content) = /\A\s*\{\s*"[^"]+"\s*=>/.match?(content.to_s)
|
|
|
774
902
|
# harmless but useless — every claim passes and the audit reads zero. A
|
|
775
903
|
# warning, never an error: the pack owns matcher quality; the engine refuses
|
|
776
904
|
# only uncompileable data.
|
|
905
|
+
# A stored agent that declares `tools_allow` / `tools_deny` /
|
|
906
|
+
# `tools_allow_groups` but does not name the `tool_allowlist` policy. Only
|
|
907
|
+
# that policy applies the lists, and the Policy::Engine runs ONLY the
|
|
908
|
+
# policies a profile names — so as written on disk the lists are inert and
|
|
909
|
+
# the model receives every registered tool (hundreds of schemas, tens of
|
|
910
|
+
# thousands of tokens, per request).
|
|
911
|
+
#
|
|
912
|
+
# AgentProfile.build now adds the policy whenever a list is declared, so a
|
|
913
|
+
# live turn off this record is already safe. The record itself stays wrong
|
|
914
|
+
# until something re-saves it, and anything that reads the stored shape
|
|
915
|
+
# directly — an older engine, a pack export, an operator auditing the
|
|
916
|
+
# Studio form — still sees an allowlist that does nothing. That is why this
|
|
917
|
+
# reads the RAW record and not `all`.
|
|
918
|
+
def check_tool_allowlist_policy
|
|
919
|
+
return [] unless @profile_source.respond_to?(:all_raw)
|
|
920
|
+
|
|
921
|
+
records = @profile_source.all_raw
|
|
922
|
+
bad = records.select { |r| tool_lists_declared?(r) && !names_tool_allowlist?(r) }
|
|
923
|
+
return [ok("tool-allowlist", "#{records.length} stored agent(s): every declared tool list names the policy")] if bad.empty?
|
|
924
|
+
|
|
925
|
+
bad.map do |r|
|
|
926
|
+
Finding.new(check: "tool-allowlist", severity: :error, fix: nil,
|
|
927
|
+
message: "agent '#{r["id"]}' declares a tool allow/deny list but its policies " \
|
|
928
|
+
"(#{Array(r["policies"]).join(", ").then { |p| p.empty? ? "none" : p }}) " \
|
|
929
|
+
"do not include tool_allowlist — as stored, the list is never applied and " \
|
|
930
|
+
"the model receives every registered tool. The engine repairs this when it " \
|
|
931
|
+
"loads the agent; re-save it (Studio, or the pack) so the stored policies " \
|
|
932
|
+
"match the intent.")
|
|
933
|
+
end
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
# Presence, not emptiness, for the two nil-able lists: `tools_allow: []`
|
|
937
|
+
# means "no tools" and is as much a declaration as a list of names.
|
|
938
|
+
# `tools_deny` has no nil state, so only a non-empty one counts.
|
|
939
|
+
def tool_lists_declared?(raw)
|
|
940
|
+
!raw["tools_allow"].nil? || !raw["tools_allow_groups"].nil? || !Array(raw["tools_deny"]).empty?
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
def names_tool_allowlist?(raw) = Array(raw["policies"]).any? { |p| p.to_s == "tool_allowlist" }
|
|
944
|
+
|
|
777
945
|
def check_grounding
|
|
778
946
|
return [] unless @profile_source
|
|
779
947
|
|
|
@@ -976,6 +1144,80 @@ def wrapped_content?(content) = /\A\s*\{\s*"[^"]+"\s*=>/.match?(content.to_s)
|
|
|
976
1144
|
e.message
|
|
977
1145
|
end
|
|
978
1146
|
|
|
1147
|
+
# the recurring-schedule check — declarations
|
|
1148
|
+
# validated where they are declared (each one through the SAME
|
|
1149
|
+
# Insika::Schedule parser the engine fires on, never a second opinion),
|
|
1150
|
+
# plus the data read that answers "what is being skipped right now".
|
|
1151
|
+
# A bare install (no schedule on any profile) reports NOTHING.
|
|
1152
|
+
def check_schedules
|
|
1153
|
+
return [] unless @profile_source
|
|
1154
|
+
|
|
1155
|
+
declared = @profile_source.all.select do |profile|
|
|
1156
|
+
profile.respond_to?(:schedules) && Array(profile.schedules).any?
|
|
1157
|
+
end
|
|
1158
|
+
return [] if declared.empty?
|
|
1159
|
+
|
|
1160
|
+
declared.flat_map do |profile|
|
|
1161
|
+
Array(profile.schedules).flat_map do |schedule|
|
|
1162
|
+
decl = Insika::Schedule.parse(schedule)
|
|
1163
|
+
if decl.nil?
|
|
1164
|
+
[Finding.new(check: "schedules", severity: :error, fix: nil,
|
|
1165
|
+
message: "agent '#{profile.id}': malformed schedule — " \
|
|
1166
|
+
"#{schedule_defect(schedule)}. The engine will never " \
|
|
1167
|
+
"fire it until this is fixed.")]
|
|
1168
|
+
else
|
|
1169
|
+
ok_finding = schedule_ok(profile, decl)
|
|
1170
|
+
[ok_finding] + schedule_data_findings(profile, decl)
|
|
1171
|
+
end
|
|
1172
|
+
end
|
|
1173
|
+
end
|
|
1174
|
+
end
|
|
1175
|
+
|
|
1176
|
+
# The ok finding for one valid declaration — plus the warning that
|
|
1177
|
+
# names the cadence ceiling: the engine claims ONE window per pass, so an
|
|
1178
|
+
# `every` shorter than the claim window cannot fire more often than it.
|
|
1179
|
+
def schedule_ok(profile, decl)
|
|
1180
|
+
trigger = decl.cron ? "cron #{decl.cron} <#{decl.tz}>" : "every #{decl.every}s"
|
|
1181
|
+
floor = Insika::ScheduleEngine::DEFAULT_WINDOW
|
|
1182
|
+
if !decl.cron.nil? || decl.every >= floor
|
|
1183
|
+
ok("schedules",
|
|
1184
|
+
"agent '#{profile.id}': schedule '#{decl.id}' — #{trigger}, " \
|
|
1185
|
+
"#{decl.fixed_session? ? "a fixed standing session" : 'a new session per run'}, " \
|
|
1186
|
+
"#{decl.enabled ? 'enabled' : 'disabled'}")
|
|
1187
|
+
else
|
|
1188
|
+
Finding.new(check: "schedules", severity: :warn, fix: nil,
|
|
1189
|
+
message: "agent '#{profile.id}': schedule '#{decl.id}' declares " \
|
|
1190
|
+
"every #{decl.every}s, but the engine fires at most once per " \
|
|
1191
|
+
"#{floor}s claim window — the effective cadence is one run " \
|
|
1192
|
+
"per ~#{floor}s, not one per #{decl.every}s.")
|
|
1193
|
+
end
|
|
1194
|
+
end
|
|
1195
|
+
|
|
1196
|
+
# The named defect, for the error message — the engine's parser's words.
|
|
1197
|
+
def schedule_defect(hash)
|
|
1198
|
+
Insika::Schedule.parse!(hash).to_s
|
|
1199
|
+
rescue Insika::ValidationError => e
|
|
1200
|
+
e.message
|
|
1201
|
+
end
|
|
1202
|
+
|
|
1203
|
+
# Data-age findings, gated on the optional collaborator (nil = skip,
|
|
1204
|
+
# env-only callers stay cheap): the row's last skip, when the schedule
|
|
1205
|
+
# was created and has one — visible in the Studio, named here too.
|
|
1206
|
+
def schedule_data_findings(profile, decl)
|
|
1207
|
+
return [] unless @schedule_store
|
|
1208
|
+
|
|
1209
|
+
row = @schedule_store.find(tenant: Insika::ScheduleEngine.tenant_for(profile),
|
|
1210
|
+
agent: profile.id.to_s, id: decl.id)
|
|
1211
|
+
return [] unless row&.last_skip
|
|
1212
|
+
|
|
1213
|
+
[Finding.new(check: "schedules", severity: :info, fix: nil,
|
|
1214
|
+
message: "agent '#{profile.id}' schedule '#{decl.id}': the last run was " \
|
|
1215
|
+
"SKIPPED at #{row.last_skip['at']} — reason " \
|
|
1216
|
+
"#{row.last_skip['reason'].inspect} (overlap = the previous " \
|
|
1217
|
+
"run still live; budget = a hard cap reached; late = the " \
|
|
1218
|
+
"window was missed by the no-catch-up policy).")]
|
|
1219
|
+
end
|
|
1220
|
+
|
|
979
1221
|
# Data-age findings, gated on the optional collaborators (nil = skip,
|
|
980
1222
|
# env-only callers stay cheap).
|
|
981
1223
|
def followup_data_findings(profile, decl)
|
|
@@ -1062,6 +1304,27 @@ def wrapped_content?(content) = /\A\s*\{\s*"[^"]+"\s*=>/.match?(content.to_s)
|
|
|
1062
1304
|
stale: @proposal_store.stale(limit: 10_000).size }
|
|
1063
1305
|
end
|
|
1064
1306
|
|
|
1307
|
+
# the in-session compaction check (RFC-0044): platform-gated,
|
|
1308
|
+
# so ONE line — enabled with no resolvable model (no compaction.model, no
|
|
1309
|
+
# platform utility_model) can never run; the warn is the same "declared
|
|
1310
|
+
# but dead" signal as check_distill's (the engine never guesses a model).
|
|
1311
|
+
def check_compaction
|
|
1312
|
+
return [] unless @settings_store
|
|
1313
|
+
|
|
1314
|
+
settings = @settings_store.get
|
|
1315
|
+
config = settings["compaction"] || {}
|
|
1316
|
+
return [ok("compaction", "in-session compaction off")] unless Coercion.truthy?(config["enabled"])
|
|
1317
|
+
|
|
1318
|
+
if Coercion.presence(config["model"]).nil? && Coercion.presence(settings["utility_model"]).nil?
|
|
1319
|
+
[Finding.new(check: "compaction", severity: :warn, fix: nil,
|
|
1320
|
+
message: "in-session compaction is enabled but has no model slot — it will " \
|
|
1321
|
+
"never run (set compaction.model or the platform utility_model).")]
|
|
1322
|
+
else
|
|
1323
|
+
[ok("compaction", "in-session compaction on — keep_last #{config['keep_last']}, " \
|
|
1324
|
+
"compact_after #{config['compact_after']}")]
|
|
1325
|
+
end
|
|
1326
|
+
end
|
|
1327
|
+
|
|
1065
1328
|
# the harvest check — per profile WITH a harvest hash:
|
|
1066
1329
|
# declared-without-model warn (D12), no grounding matcher warn (D3),
|
|
1067
1330
|
# malformed negative list error (D4), else ok with the pending counts.
|
|
@@ -11,11 +11,12 @@ module Insika
|
|
|
11
11
|
# exactly like any other pack. That is what makes `#profile` identical to a
|
|
12
12
|
# hand-written equivalent pack (the parity spec) — there is only one path.
|
|
13
13
|
class Definition
|
|
14
|
-
attr_reader :pack, :runtime_options
|
|
14
|
+
attr_reader :pack, :runtime_options, :mcp_instances
|
|
15
15
|
|
|
16
|
-
def initialize(pack:, runtime: {})
|
|
16
|
+
def initialize(pack:, runtime: {}, mcp_instances: [])
|
|
17
17
|
@pack = pack
|
|
18
18
|
@runtime_options = runtime
|
|
19
|
+
@mcp_instances = mcp_instances
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def id = pack.config[:id].to_s
|
data/lib/insika/dsl/runtime.rb
CHANGED
|
@@ -14,8 +14,6 @@ module Insika
|
|
|
14
14
|
# Built lazily by Definition#runtime, so `require "insika"` never pays for
|
|
15
15
|
# ruby_llm or the HTTP server.
|
|
16
16
|
class Runtime
|
|
17
|
-
TERMINAL = %i[task_completed task_failed task_cancelled].freeze
|
|
18
|
-
|
|
19
17
|
# graph: the assembled Wiring::Graph::Result; pack: the PRIMARY imported
|
|
20
18
|
# Pack; packs: every imported Pack (one per agent — a System has N, a
|
|
21
19
|
# Definition has one). All read by the server boot and the specs.
|
|
@@ -40,6 +38,11 @@ module Insika
|
|
|
40
38
|
# rather than reading a process-wide singleton at call time.
|
|
41
39
|
@llm = build_llm_context
|
|
42
40
|
@graph = build_graph
|
|
41
|
+
# the dispatch+drain seam `#chat`/`#run_workflow` delegate to —
|
|
42
|
+
# `Insika::Wiring::GraphChat`, shared with `config/deployment.rb`'s own
|
|
43
|
+
# registration of `run_persona_eval` (see `register_persona_eval_tool`
|
|
44
|
+
# below).
|
|
45
|
+
@chat_runtime = Insika::Wiring::GraphChat.new(graph: @graph)
|
|
43
46
|
seed_default_model
|
|
44
47
|
import_packs
|
|
45
48
|
end
|
|
@@ -62,12 +65,8 @@ module Insika
|
|
|
62
65
|
# one-shot; a value threads multi-turn memory (created on first use).
|
|
63
66
|
# `agent:` targets one agent of a system (default: the primary).
|
|
64
67
|
def chat(message, session_id: nil, timeout: nil, agent: nil)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
{ agent: (agent || @definition.id).to_s, message: message, session_id: session_id },
|
|
68
|
-
transport: :cli
|
|
69
|
-
)
|
|
70
|
-
run_command(command, session_id: session_id, timeout: timeout)[:text]
|
|
68
|
+
@chat_runtime.chat(message, agent: (agent || @definition.id).to_s,
|
|
69
|
+
session_id: session_id, timeout: timeout)
|
|
71
70
|
end
|
|
72
71
|
|
|
73
72
|
# Triggers a declared workflow and returns its OUTPUT (the typed value the
|
|
@@ -78,7 +77,7 @@ module Insika
|
|
|
78
77
|
command = Insika::Command.build(
|
|
79
78
|
:trigger_workflow, { workflow: name, agent: agent, input: input }, transport: :cli
|
|
80
79
|
)
|
|
81
|
-
outcome = run_command(command, session_id: nil, timeout: timeout)
|
|
80
|
+
outcome = @chat_runtime.run_command(command, session_id: nil, timeout: timeout)
|
|
82
81
|
outcome.key?(:output) ? outcome[:output] : outcome[:text]
|
|
83
82
|
end
|
|
84
83
|
|
|
@@ -93,70 +92,6 @@ module Insika
|
|
|
93
92
|
|
|
94
93
|
private
|
|
95
94
|
|
|
96
|
-
# Dispatch + drain, shared by a turn and a workflow run: subscribe BEFORE
|
|
97
|
-
# dispatching (the fiber may emit eagerly), bind to the task, read to the
|
|
98
|
-
# terminal event. A synchronous handler error (unknown agent/workflow, bad
|
|
99
|
-
# input against the schema) propagates from `dispatch` untouched — the
|
|
100
|
-
# caller sees the real ValidationError/NotFoundError, not a turn failure.
|
|
101
|
-
def run_command(command, session_id:, timeout:)
|
|
102
|
-
require "async"
|
|
103
|
-
outcome = {}
|
|
104
|
-
rejected = nil
|
|
105
|
-
Async do |task|
|
|
106
|
-
serving = !session_id.nil?
|
|
107
|
-
@graph.executor.supervised = true if serving
|
|
108
|
-
ensure_session(session_id) if session_id
|
|
109
|
-
sub = @graph.event_stream.subscribe
|
|
110
|
-
res = begin
|
|
111
|
-
@graph.bus.dispatch(command)
|
|
112
|
-
rescue StandardError => e
|
|
113
|
-
# CAPTURED, not re-raised inside the reactor: letting it escape the
|
|
114
|
-
# Async block logs "Task may have ended with unhandled exception" —
|
|
115
|
-
# alarming noise for a DOCUMENTED rejection (a schema violation, an
|
|
116
|
-
# unknown agent). Re-raised verbatim below, outside the reactor.
|
|
117
|
-
sub.close
|
|
118
|
-
rejected = e
|
|
119
|
-
next
|
|
120
|
-
end
|
|
121
|
-
sub.bind(task_id: res[:task_id])
|
|
122
|
-
drain(sub, outcome, task, timeout)
|
|
123
|
-
teardown_serving if serving
|
|
124
|
-
end.wait
|
|
125
|
-
raise rejected if rejected
|
|
126
|
-
raise Insika::Error, "turn #{outcome[:error]}" if outcome[:error]
|
|
127
|
-
|
|
128
|
-
outcome
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# Reads the stream until this turn reaches a terminal event, then stops.
|
|
132
|
-
# A workflow run also carries its typed OUTPUT, which is not the turn text.
|
|
133
|
-
def drain(sub, outcome, task, timeout)
|
|
134
|
-
reader = task.async do
|
|
135
|
-
sub.each do |ev|
|
|
136
|
-
case ev.type
|
|
137
|
-
when :task_completed then outcome[:text] = ev.data[:content].to_s
|
|
138
|
-
when :workflow_completed then outcome[:output] = ev.data[:output]
|
|
139
|
-
when :task_failed then outcome[:error] = "failed: #{ev.data[:message] || ev.data[:error]}"
|
|
140
|
-
when :task_cancelled then outcome[:error] = "cancelled"
|
|
141
|
-
end
|
|
142
|
-
sub.close if TERMINAL.include?(ev.type)
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
timeout ? task.with_timeout(timeout) { reader.wait } : reader.wait
|
|
146
|
-
rescue Async::TimeoutError
|
|
147
|
-
outcome[:error] = "timed out after #{timeout}s"
|
|
148
|
-
sub.close
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def ensure_session(session_id)
|
|
152
|
-
@graph.session_store.find(session_id) || @graph.session_store.create(id: session_id, vars: {})
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def teardown_serving
|
|
156
|
-
@graph.executor.stop_session_actors
|
|
157
|
-
@graph.executor.instance_variable_get(:@supervisor)&.stop
|
|
158
|
-
end
|
|
159
|
-
|
|
160
95
|
# --- graph assembly (mirrors config/deployment.rb, generically) ------
|
|
161
96
|
|
|
162
97
|
def build_graph
|
|
@@ -183,6 +118,20 @@ module Insika
|
|
|
183
118
|
)
|
|
184
119
|
register_authoring_commands(graph, c)
|
|
185
120
|
register_workflows(graph)
|
|
121
|
+
import_mcp_declarations(c)
|
|
122
|
+
# `run_persona_eval` (C3.1): the SAME registration `config/deployment.rb`
|
|
123
|
+
# does for its own graph — see `Wiring::Graph.register_persona_eval_tool`.
|
|
124
|
+
# One implementation, both roots; this one just already has its
|
|
125
|
+
# `golden_store`/`settings_store` at hand from `build_components`.
|
|
126
|
+
Insika::Wiring::Graph.register_persona_eval_tool(
|
|
127
|
+
graph, golden_store: Insika::GoldenStore.new(config_store: c[:config_store]),
|
|
128
|
+
settings_store: c[:settings_store], llm: @llm
|
|
129
|
+
)
|
|
130
|
+
# Same boot step the server roots run: an installed `insika-plugin-*`
|
|
131
|
+
# gem (or INSIKA_PLUGIN_DIR) extends a DSL-run agent too — a plugin
|
|
132
|
+
# works everywhere or it is not a plugin. No bundled root here: the DSL
|
|
133
|
+
# runs outside the repo checkout.
|
|
134
|
+
Insika::Wiring::Graph.load_plugins(graph)
|
|
186
135
|
graph
|
|
187
136
|
end
|
|
188
137
|
|
|
@@ -210,15 +159,42 @@ module Insika
|
|
|
210
159
|
))
|
|
211
160
|
end
|
|
212
161
|
|
|
162
|
+
# Upserts every DSL-declared `mcp` instance into the McpStore at boot.
|
|
163
|
+
# Code is the TEMPLATE — transport/command/args/url/
|
|
164
|
+
# description always follow the declaration — but `enabled` and the
|
|
165
|
+
# credentials (`env`/`headers`) are the operator's once the instance
|
|
166
|
+
# exists: an already-present record's own values are re-sent verbatim
|
|
167
|
+
# instead of the DSL's, so a Studio/CLI/API edit made after boot is never
|
|
168
|
+
# clobbered by the next restart (motor-vs-forja: the store's DATA wins).
|
|
169
|
+
def import_mcp_declarations(c)
|
|
170
|
+
declared = Array(@definition.mcp_instances)
|
|
171
|
+
return if declared.empty?
|
|
172
|
+
|
|
173
|
+
mcp_store = c[:mcp_store]
|
|
174
|
+
declared.each do |decl|
|
|
175
|
+
attrs = decl.dup
|
|
176
|
+
existing = mcp_store.get_raw(attrs[:name])
|
|
177
|
+
if existing
|
|
178
|
+
attrs[:enabled] = existing["enabled"]
|
|
179
|
+
attrs[:env] = existing["env"]
|
|
180
|
+
attrs[:headers] = existing["headers"]
|
|
181
|
+
end
|
|
182
|
+
mcp_store.upsert(attrs)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
213
186
|
def build_components(backend, spine)
|
|
214
187
|
config_store = Insika::ConfigStore.new(store: backend)
|
|
215
188
|
settings_store = Insika::SettingsStore.new(config_store: config_store)
|
|
216
189
|
provider_store = Insika::LLMProviderStore.new(config_store: config_store)
|
|
217
190
|
tool_store = Insika::ToolStore.new(config_store: config_store)
|
|
218
191
|
skill_store = Insika::SkillStore.new(config_store: config_store)
|
|
192
|
+
mcp_store = Insika::McpStore.new(config_store: config_store)
|
|
193
|
+
mcp_tool_registry = Insika::McpToolRegistry.new(mcp_store: mcp_store)
|
|
219
194
|
tool_registry = Insika::OverlayToolRegistry.new(
|
|
220
195
|
base: spine.code_tool_registry, tool_store: tool_store,
|
|
221
|
-
http: Insika::HttpClient.new, event_stream: spine.event_stream
|
|
196
|
+
http: Insika::HttpClient.new, event_stream: spine.event_stream,
|
|
197
|
+
mcp_registry: mcp_tool_registry
|
|
222
198
|
)
|
|
223
199
|
{
|
|
224
200
|
backend: backend, config_store: config_store,
|
|
@@ -227,7 +203,7 @@ module Insika
|
|
|
227
203
|
agent_file_store: Insika::AgentFileStore.new(config_store: config_store),
|
|
228
204
|
skill_store: skill_store, tool_store: tool_store,
|
|
229
205
|
system_file_store: Insika::SystemFileStore.new(config_store: config_store),
|
|
230
|
-
mcp_store:
|
|
206
|
+
mcp_store: mcp_store, mcp_tool_registry: mcp_tool_registry,
|
|
231
207
|
tool_trace_store: Insika::ToolTraceStore.new(store: backend),
|
|
232
208
|
context_trace_store: Insika::ContextTraceStore.new(store: backend),
|
|
233
209
|
# the per-AGENT cache-hit series (the Studio agent-detail card).
|
|
@@ -252,15 +228,20 @@ module Insika
|
|
|
252
228
|
[
|
|
253
229
|
Insika::Context::Providers::Request.new,
|
|
254
230
|
Insika::Context::Providers::Prompt.new(
|
|
255
|
-
base: "",
|
|
231
|
+
base: "", catalog: c[:prompt_catalog],
|
|
256
232
|
agent_files: c[:agent_file_store], system_files: c[:system_file_store]
|
|
257
233
|
),
|
|
258
234
|
Insika::Context::Providers::Skill.new(catalog: c[:skill_catalog]),
|
|
259
235
|
Insika::Context::Providers::SkillTrigger.new(catalog: c[:skill_catalog]),
|
|
260
236
|
Insika::Context::Providers::ToolSearch.new(catalog: c[:tool_catalog]),
|
|
261
237
|
Insika::Context::Providers::Memory.new(store: spine.memory_store),
|
|
238
|
+
Insika::Context::Providers::Knowledge.new(store: spine.knowledge_store),
|
|
239
|
+
# Session briefing: read path. Inert for agents without briefing_fields.
|
|
240
|
+
# Before Session, so the durable head renders above the transcript and
|
|
241
|
+
# the tail recitation lands after it.
|
|
242
|
+
Insika::Context::Providers::Briefing.new(session_store: spine.session_store),
|
|
262
243
|
Insika::Context::Providers::Session.new(session_store: spine.session_store)
|
|
263
|
-
]
|
|
244
|
+
] # NOT frozen: load_plugins appends plugin providers at boot
|
|
264
245
|
end
|
|
265
246
|
|
|
266
247
|
# The runtime-authoring surface a pack import needs (create_agent +
|
|
@@ -288,14 +269,18 @@ module Insika
|
|
|
288
269
|
bus.register(:update_settings, Insika::Commands::UpdateSettings.new(settings_store: c[:settings_store], event_stream: es))
|
|
289
270
|
bus.register(:upsert_llm_provider, Insika::Commands::UpsertLLMProvider.new(provider_store: c[:provider_store], configurator: c[:configurator], event_stream: es))
|
|
290
271
|
bus.register(:delete_llm_provider, Insika::Commands::DeleteLLMProvider.new(provider_store: c[:provider_store], configurator: c[:configurator], event_stream: es))
|
|
291
|
-
bus.register(:upsert_mcp, Insika::Commands::UpsertMcp.new(mcp_store: c[:mcp_store], event_stream: es))
|
|
292
|
-
bus.register(:delete_mcp, Insika::Commands::DeleteMcp.new(mcp_store: c[:mcp_store], event_stream: es))
|
|
272
|
+
bus.register(:upsert_mcp, Insika::Commands::UpsertMcp.new(mcp_store: c[:mcp_store], mcp_registry: c[:mcp_tool_registry], event_stream: es))
|
|
273
|
+
bus.register(:delete_mcp, Insika::Commands::DeleteMcp.new(mcp_store: c[:mcp_store], mcp_registry: c[:mcp_tool_registry], event_stream: es))
|
|
274
|
+
bus.register(:refresh_mcp_tools, Insika::Commands::RefreshMcpTools.new(mcp_registry: c[:mcp_tool_registry], event_stream: es))
|
|
293
275
|
bus.register(:write_system_file, Insika::Commands::WriteSystemFile.new(system_file_store: c[:system_file_store], event_stream: es))
|
|
294
276
|
bus.register(:delete_system_file, Insika::Commands::DeleteSystemFile.new(system_file_store: c[:system_file_store], event_stream: es))
|
|
295
277
|
bus.register(:restore_system_file, Insika::Commands::RestoreSystemFile.new(system_file_store: c[:system_file_store], event_stream: es))
|
|
296
278
|
bus.register(:write_data_tool, Insika::Commands::WriteDataTool.new(tool_store: c[:tool_store], registry: c[:tool_registry], tool_catalog: c[:tool_catalog], event_stream: es))
|
|
297
279
|
bus.register(:delete_data_tool, Insika::Commands::DeleteDataTool.new(tool_store: c[:tool_store], registry: c[:tool_registry], tool_catalog: c[:tool_catalog], event_stream: es))
|
|
298
280
|
bus.register(:restore_data_tool, Insika::Commands::RestoreDataTool.new(tool_store: c[:tool_store], registry: c[:tool_registry], tool_catalog: c[:tool_catalog], event_stream: es))
|
|
281
|
+
bus.register(:write_concept, Insika::Commands::WriteConcept.new(knowledge_store: graph.knowledge_store, event_stream: es))
|
|
282
|
+
bus.register(:delete_concept, Insika::Commands::DeleteConcept.new(knowledge_store: graph.knowledge_store, event_stream: es))
|
|
283
|
+
bus.register(:restore_concept, Insika::Commands::RestoreConcept.new(knowledge_store: graph.knowledge_store, event_stream: es))
|
|
299
284
|
end
|
|
300
285
|
|
|
301
286
|
# --- RubyLLM configuration ------------------------------------------
|
|
@@ -51,6 +51,10 @@ module Insika
|
|
|
51
51
|
Insika::Shutdown.install(executor: @graph.executor)
|
|
52
52
|
Async do
|
|
53
53
|
@graph.executor.supervised = true # serving mode: turns survive disconnects
|
|
54
|
+
# forces the tick/alert/distill/harvest supervisor up now — otherwise a
|
|
55
|
+
# deployment with only scheduled agents (no live chat) never ticks until
|
|
56
|
+
# unrelated traffic happens to hit turn_parent first.
|
|
57
|
+
@graph.executor.start_supervisor!
|
|
54
58
|
Insika::Telemetry.attach(event_stream: @graph.event_stream, recorder: telemetry)
|
|
55
59
|
Async::HTTP::Server.new(middleware, endpoint).run
|
|
56
60
|
end
|
|
@@ -122,10 +126,28 @@ module Insika
|
|
|
122
126
|
# (nil = the page renders the refusal/empty states).
|
|
123
127
|
harvest_store: @graph.harvest_store,
|
|
124
128
|
harvest_criterion: harvest_criterion,
|
|
125
|
-
negative_list: harvest_negative_list
|
|
129
|
+
negative_list: harvest_negative_list,
|
|
130
|
+
# the Knowledge page reads the store directly; its
|
|
131
|
+
# mutations (write/delete/restore) dispatch bus commands.
|
|
132
|
+
knowledge_store: @graph.knowledge_store,
|
|
133
|
+
# the Artifacts tab reads the report store; the signing
|
|
134
|
+
# config (nil without INSIKA_ARTIFACT_SIGNING_KEY) decides whether a
|
|
135
|
+
# signed sharing link exists at all.
|
|
136
|
+
artifact_store: @graph.artifact_store,
|
|
137
|
+
artifact_signing: artifact_signing
|
|
126
138
|
)
|
|
127
139
|
end
|
|
128
140
|
|
|
141
|
+
# -> { key:, ttl:, base_url: } | nil (no signing key -> no signed surface).
|
|
142
|
+
def artifact_signing
|
|
143
|
+
key = Insika::EnvSchema.read("INSIKA_ARTIFACT_SIGNING_KEY")
|
|
144
|
+
return nil if key.to_s.empty?
|
|
145
|
+
|
|
146
|
+
{ key: key,
|
|
147
|
+
ttl: (Insika::EnvSchema.read("INSIKA_ARTIFACT_SIGNING_TTL") || 604_800).to_i,
|
|
148
|
+
base_url: Insika::Coercion.presence(Insika::EnvSchema.read("INSIKA_PUBLIC_URL")).to_s }
|
|
149
|
+
end
|
|
150
|
+
|
|
129
151
|
# The frozen criterion — best-effort at boot: a bare install
|
|
130
152
|
# without the env var renders the ruler's hole instead of
|
|
131
153
|
# crashing the Studio.
|