insika 0.8.0 → 1.0.1
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 +312 -2
- data/README.md +10 -6
- data/bin/insika +4 -1
- data/bin/insika-router +0 -1
- data/docs/AGENTS.md +142 -18
- data/docs/API.md +73 -0
- data/docs/ARCHITECTURE.md +56 -50
- data/docs/ARTIFACTS.md +33 -3
- data/docs/BENCHMARK.md +50 -7
- data/docs/CHANNELS.md +19 -2
- data/docs/CONTEXT.md +96 -27
- data/docs/CROSS-HARNESS-BENCH.md +157 -0
- data/docs/DEPLOY.md +70 -13
- data/docs/EMBEDDING.md +10 -0
- data/docs/EVALS.md +129 -10
- data/docs/FACTS.md +12 -6
- data/docs/HARVEST.md +8 -3
- data/docs/KNOWLEDGE.md +14 -0
- data/docs/MEDIA.md +6 -2
- data/docs/OBSERVABILITY.md +164 -28
- data/docs/PERFORMANCE.md +110 -0
- data/docs/PLUGINS.md +4 -4
- data/docs/POLICY.md +26 -14
- data/docs/RELEASING.md +9 -0
- data/docs/ROUTER.md +7 -0
- data/docs/RUBYLLM_2_MIGRATION.md +307 -0
- data/docs/SECURITY.md +34 -1
- data/docs/SHARED-CONVERSATIONS.md +40 -0
- data/docs/SKILLS.md +5 -3
- data/docs/STUDIO.md +122 -0
- data/docs/TOOLS.md +238 -42
- data/docs/operate.md +3 -0
- data/docs/prompts/ADD-TOOL.md +12 -2
- data/docs/prompts/DIAGNOSE-TURN.md +3 -0
- data/docs/prompts/GO-LIVE.md +3 -1
- data/lib/insika/agent_defaults.rb +140 -0
- data/lib/insika/agent_file_store.rb +12 -0
- data/lib/insika/agent_profile.rb +99 -17
- data/lib/insika/artifact_signing.rb +11 -0
- data/lib/insika/channels/web/widget.js +33 -0
- data/lib/insika/channels/web.rb +5 -2
- data/lib/insika/chat_builder.rb +124 -28
- data/lib/insika/checkpoint.rb +6 -1
- data/lib/insika/checkpoint_store.rb +59 -7
- data/lib/insika/commands/agent_payload.rb +3 -3
- data/lib/insika/commands/delete_kit.rb +26 -0
- data/lib/insika/commands/gate_harvest.rb +3 -0
- data/lib/insika/commands/review_guardrail_flag.rb +38 -0
- data/lib/insika/commands/run_distillation.rb +5 -8
- data/lib/insika/commands/run_harvest.rb +17 -1
- data/lib/insika/commands/seed_session.rb +128 -0
- data/lib/insika/commands/send_message.rb +9 -3
- data/lib/insika/commands/update_settings.rb +23 -1
- data/lib/insika/commands/write_kit.rb +27 -0
- data/lib/insika/compaction.rb +33 -7
- data/lib/insika/config_store.rb +87 -7
- data/lib/insika/context/builder.rb +29 -9
- data/lib/insika/context/priority.rb +5 -0
- data/lib/insika/context/provider.rb +7 -2
- data/lib/insika/context/providers/briefing.rb +11 -8
- data/lib/insika/context/providers/fence_notice.rb +27 -0
- data/lib/insika/context/providers/knowledge.rb +32 -6
- data/lib/insika/context/providers/memory.rb +61 -8
- data/lib/insika/context/providers/pending_confirmation.rb +52 -0
- data/lib/insika/context/providers/prompt.rb +17 -4
- data/lib/insika/context/providers/session.rb +46 -6
- data/lib/insika/context/reranker.rb +75 -0
- data/lib/insika/distill.rb +22 -13
- data/lib/insika/doctor.rb +142 -4
- data/lib/insika/dsl/runtime.rb +37 -4
- data/lib/insika/dsl/server_boot.rb +4 -12
- data/lib/insika/dsl.rb +19 -5
- data/lib/insika/edge_limiter.rb +27 -16
- data/lib/insika/egress_guard.rb +11 -3
- data/lib/insika/env_schema.rb +33 -73
- data/lib/insika/errors.rb +1 -0
- data/lib/insika/evals/assertions.rb +259 -18
- data/lib/insika/evals/golden.rb +230 -4
- data/lib/insika/evals/judge.rb +19 -10
- data/lib/insika/evals/moderator_compare.rb +47 -0
- data/lib/insika/evals/report.rb +61 -9
- data/lib/insika/evals/runner.rb +64 -4
- data/lib/insika/evals/simulator.rb +21 -3
- data/lib/insika/evals/transport.rb +317 -15
- data/lib/insika/event_stream.rb +63 -7
- data/lib/insika/evidence.rb +105 -17
- data/lib/insika/executor.rb +698 -164
- data/lib/insika/fence.rb +96 -0
- data/lib/insika/golden_store.rb +3 -0
- data/lib/insika/harvest.rb +26 -14
- data/lib/insika/harvest_engine.rb +1 -0
- data/lib/insika/harvest_store.rb +12 -0
- data/lib/insika/http_client.rb +87 -4
- data/lib/insika/kits.rb +54 -0
- data/lib/insika/knowledge.rb +34 -20
- data/lib/insika/llm_configurator.rb +46 -4
- data/lib/insika/llm_provider_store.rb +1 -3
- data/lib/insika/llm_trace_store.rb +54 -0
- data/lib/insika/mcp_client.rb +27 -63
- data/lib/insika/mcp_live_tool.rb +55 -9
- data/lib/insika/mcp_store.rb +37 -3
- data/lib/insika/mcp_tool_registry.rb +76 -26
- data/lib/insika/media.rb +38 -25
- data/lib/insika/memory_store.rb +16 -3
- data/lib/insika/memory_trim.rb +60 -0
- data/lib/insika/middleware.rb +12 -3
- data/lib/insika/model_metrics_store.rb +285 -0
- data/lib/insika/model_registry.rb +110 -0
- data/lib/insika/model_resolver.rb +1 -1
- data/lib/insika/model_selection.rb +14 -21
- data/lib/insika/model_visible.rb +17 -6
- data/lib/insika/onboarding.rb +1 -0
- data/lib/insika/outcome_store.rb +1 -1
- data/lib/insika/overlay_tool_registry.rb +30 -2
- data/lib/insika/pack_importer.rb +54 -9
- data/lib/insika/packaging.rb +14 -5
- data/lib/insika/pending_action_store.rb +45 -18
- data/lib/insika/plugin/loader.rb +4 -2
- data/lib/insika/prefix_fingerprint.rb +32 -27
- data/lib/insika/profile_source.rb +5 -0
- data/lib/insika/provider_error_classifier.rb +18 -3
- data/lib/insika/queue_policy.rb +7 -2
- data/lib/insika/refinement/proposer.rb +42 -17
- data/lib/insika/reliability.rb +12 -3
- data/lib/insika/router/app.rb +14 -5
- data/lib/insika/router/session_key.rb +35 -5
- data/lib/insika/safety/config.rb +20 -3
- data/lib/insika/safety/corpus.rb +10 -3
- data/lib/insika/safety/factory.rb +39 -1
- data/lib/insika/safety/input_guardrail.rb +21 -1
- data/lib/insika/safety/judge_moderator.rb +70 -0
- data/lib/insika/safety/output_validator.rb +29 -5
- data/lib/insika/safety/prompt_echo.rb +89 -0
- data/lib/insika/schema_guard.rb +53 -76
- data/lib/insika/server/app.rb +58 -7
- data/lib/insika/server/rack_app.rb +2 -0
- data/lib/insika/server/responses.rb +50 -5
- data/lib/insika/session_actor.rb +58 -28
- data/lib/insika/session_store.rb +163 -11
- data/lib/insika/settings_store.rb +57 -2
- data/lib/insika/shared_conversations.rb +305 -0
- data/lib/insika/skill_catalog.rb +28 -2
- data/lib/insika/skill_store.rb +6 -0
- data/lib/insika/soak/runner.rb +1 -1
- data/lib/insika/spoken_transcript.rb +31 -0
- data/lib/insika/steer_injector.rb +7 -48
- data/lib/insika/store.rb +29 -1
- data/lib/insika/stores/json_value.rb +37 -0
- data/lib/insika/stores/memory.rb +22 -35
- data/lib/insika/stores/postgres.rb +295 -0
- data/lib/insika/stores/sqlite.rb +257 -43
- data/lib/insika/stores/turn_counter.rb +25 -0
- data/lib/insika/studio/app.rb +1459 -163
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +25 -25
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-sans-latin-wght-normal.woff2 +0 -0
- data/lib/insika/studio/forms.rb +189 -12
- data/lib/insika/studio/nav_icons.rb +3 -0
- data/lib/insika/studio/views/_agent_config_retrieval.erb +73 -0
- data/lib/insika/studio/views/_agent_picker.erb +15 -0
- data/lib/insika/studio/views/_agent_tab_config.erb +83 -243
- data/lib/insika/studio/views/_agent_tab_history.erb +56 -1
- data/lib/insika/studio/views/_agent_tab_performance.erb +34 -0
- data/lib/insika/studio/views/_agent_tab_prompts.erb +7 -9
- data/lib/insika/studio/views/_agent_tab_skills.erb +16 -7
- data/lib/insika/studio/views/_agent_tab_tools.erb +90 -0
- data/lib/insika/studio/views/_agents_master.erb +8 -12
- data/lib/insika/studio/views/_cfg_alerts.erb +4 -0
- data/lib/insika/studio/views/_cfg_budget.erb +20 -0
- data/lib/insika/studio/views/_cfg_distill.erb +26 -0
- data/lib/insika/studio/views/_cfg_followup.erb +30 -0
- data/lib/insika/studio/views/_cfg_grounding.erb +21 -0
- data/lib/insika/studio/views/_cfg_guardrails.erb +47 -0
- data/lib/insika/studio/views/_cfg_harvest.erb +35 -0
- data/lib/insika/studio/views/_cfg_reliability.erb +32 -0
- data/lib/insika/studio/views/_chats_master.erb +66 -0
- data/lib/insika/studio/views/_customers_master.erb +37 -0
- data/lib/insika/studio/views/_empty.erb +15 -0
- data/lib/insika/studio/views/_facts_master.erb +42 -0
- data/lib/insika/studio/views/_harvest_candidate_detail.erb +14 -0
- data/lib/insika/studio/views/_identity.erb +16 -0
- data/lib/insika/studio/views/_kpi.erb +17 -0
- data/lib/insika/studio/views/_message.erb +40 -2
- data/lib/insika/studio/views/_model_chart.erb +82 -0
- data/lib/insika/studio/views/_refinement_master.erb +38 -0
- data/lib/insika/studio/views/{tool_edit.erb → _tool_edit.erb} +38 -11
- data/lib/insika/studio/views/_tool_users.erb +17 -0
- data/lib/insika/studio/views/_why.erb +53 -0
- data/lib/insika/studio/views/agent_detail.erb +17 -10
- data/lib/insika/studio/views/agents.erb +28 -16
- data/lib/insika/studio/views/approvals.erb +16 -11
- data/lib/insika/studio/views/artifact.erb +20 -7
- data/lib/insika/studio/views/artifacts.erb +19 -14
- data/lib/insika/studio/views/chats.erb +22 -29
- data/lib/insika/studio/views/customer.erb +88 -43
- data/lib/insika/studio/views/customers.erb +16 -26
- data/lib/insika/studio/views/evals.erb +49 -31
- data/lib/insika/studio/views/fact_detail.erb +114 -0
- data/lib/insika/studio/views/facts.erb +15 -116
- data/lib/insika/studio/views/followups.erb +63 -52
- data/lib/insika/studio/views/funnel.erb +104 -43
- data/lib/insika/studio/views/harvest.erb +85 -59
- data/lib/insika/studio/views/home.erb +138 -23
- data/lib/insika/studio/views/inbox.erb +36 -0
- data/lib/insika/studio/views/kits.erb +176 -0
- data/lib/insika/studio/views/knowledge.erb +92 -80
- data/lib/insika/studio/views/layout.erb +42 -11
- data/lib/insika/studio/views/live.erb +45 -0
- data/lib/insika/studio/views/mcp.erb +43 -13
- data/lib/insika/studio/views/models.erb +187 -0
- data/lib/insika/studio/views/parity.erb +38 -32
- data/lib/insika/studio/views/playground.erb +13 -9
- data/lib/insika/studio/views/refinement.erb +15 -224
- data/lib/insika/studio/views/refinement_detail.erb +243 -0
- data/lib/insika/studio/views/session.erb +153 -118
- data/lib/insika/studio/views/settings.erb +86 -22
- data/lib/insika/studio/views/skills.erb +53 -19
- data/lib/insika/studio/views/system_files.erb +7 -7
- data/lib/insika/studio/views/task.erb +45 -15
- data/lib/insika/studio/views/tasks.erb +35 -18
- data/lib/insika/studio/views/tools.erb +62 -73
- data/lib/insika/task_actor.rb +4 -1
- data/lib/insika/task_store.rb +67 -1
- data/lib/insika/telemetry/pricing.rb +16 -14
- data/lib/insika/telemetry/recorder.rb +115 -9
- data/lib/insika/telemetry/ruby_llm_instrumenter.rb +110 -0
- data/lib/insika/telemetry.rb +12 -2
- data/lib/insika/templates/daily-digest/README.md +13 -0
- data/lib/insika/templates/daily-digest/agent.rb +22 -11
- data/lib/insika/testing/store_contract.rb +65 -0
- data/lib/insika/text_tool_call.rb +66 -0
- data/lib/insika/tick.rb +7 -1
- data/lib/insika/token_store.rb +5 -0
- data/lib/insika/tool_assembly.rb +21 -13
- data/lib/insika/tool_batch.rb +1 -1
- data/lib/insika/tool_catalog.rb +12 -2
- data/lib/insika/tool_definition.rb +82 -13
- data/lib/insika/tool_envelope.rb +266 -29
- data/lib/insika/tool_manifest.rb +1 -0
- data/lib/insika/tool_store.rb +12 -4
- data/lib/insika/tool_trace_store.rb +15 -8
- data/lib/insika/tool_usage_report.rb +14 -4
- data/lib/insika/tools/a2a_remote.rb +1 -1
- data/lib/insika/tools/agent_enum.rb +1 -1
- data/lib/insika/tools/confirm_pending.rb +112 -0
- data/lib/insika/tools/data_defined_tool.rb +24 -15
- data/lib/insika/tools/generate_image.rb +2 -2
- data/lib/insika/tools/load_knowledge.rb +7 -3
- data/lib/insika/tools/load_skill.rb +3 -3
- data/lib/insika/tools/present.rb +115 -0
- data/lib/insika/tools/remember.rb +7 -2
- data/lib/insika/tools/run_persona_eval.rb +9 -6
- data/lib/insika/tools/save_artifact.rb +16 -6
- data/lib/insika/tools/schedule_followup.rb +3 -3
- data/lib/insika/tools/stuck_signal.rb +3 -3
- data/lib/insika/tools/subagent.rb +5 -5
- data/lib/insika/tools/subagents.rb +3 -3
- data/lib/insika/tools/tool_search.rb +11 -7
- data/lib/insika/tools/tts.rb +2 -2
- data/lib/insika/tools/update_briefing.rb +4 -4
- data/lib/insika/turn_output.rb +2 -2
- data/lib/insika/turn_state.rb +31 -1
- data/lib/insika/turn_timing.rb +51 -14
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +88 -16
- data/lib/insika/wiring/graph_chat.rb +4 -0
- data/lib/insika/workflow.rb +25 -64
- data/lib/insika.rb +23 -3
- metadata +74 -13
- data/lib/insika/commands/import_mcp_tools.rb +0 -48
- data/lib/insika/mcp_http_client.rb +0 -67
- data/lib/insika/mcp_tool_ingestor.rb +0 -165
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
# Platform defaults for agent fields (Settings["agent_defaults"]): an agent
|
|
5
|
+
# with no value of its own for an INHERITABLE field runs with the platform's.
|
|
6
|
+
# The agent's own value always wins, whole (no merge of the two).
|
|
7
|
+
#
|
|
8
|
+
# Applied only where turns read profiles (the graph, through ProfileSource
|
|
9
|
+
# below). Authoring (Studio, pack import) reads the agent's own record, so
|
|
10
|
+
# saving an agent never copies a default into it and it keeps inheriting.
|
|
11
|
+
#
|
|
12
|
+
# FIELDS are the ones where "absent" already means "not configured". Fields
|
|
13
|
+
# where absent means something else (tools_allow nil = every tool) or where
|
|
14
|
+
# absent and off read the same (the memory/fencing booleans) are left out.
|
|
15
|
+
module AgentDefaults
|
|
16
|
+
FIELDS = %w[reliability knowledge memory_retrieval budget alerts guardrails
|
|
17
|
+
grounding followup distill harvest].freeze
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# -> the profile, rebuilt with the defaults it lacks (the same object when
|
|
22
|
+
# there is nothing to fill).
|
|
23
|
+
def apply(profile, defaults)
|
|
24
|
+
fill = FIELDS.each_with_object({}) do |field, acc|
|
|
25
|
+
value = defaults[field]
|
|
26
|
+
acc[field.to_sym] = value if !value.nil? && profile.public_send(field).nil?
|
|
27
|
+
end
|
|
28
|
+
return profile if fill.empty?
|
|
29
|
+
|
|
30
|
+
AgentProfile.build(**profile.to_h.merge(fill))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Like #apply, but a default the agent cannot be built with (saved before it
|
|
34
|
+
# was validated, or a rerank model the registry dropped since) leaves the
|
|
35
|
+
# agent on its own values: one bad default must not stop every turn.
|
|
36
|
+
def apply_safely(profile, defaults)
|
|
37
|
+
apply(profile, defaults)
|
|
38
|
+
rescue Insika::ValidationError => e
|
|
39
|
+
warn "[insika] agent defaults not applied to #{profile.id}: #{e.message}"
|
|
40
|
+
profile
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Raises ValidationError unless an agent can be built with these defaults.
|
|
44
|
+
def validate!(defaults)
|
|
45
|
+
fields = defaults.to_h { |field, value| [field.to_sym, value] }
|
|
46
|
+
AgentProfile.build(id: "agent-defaults", model: nil, **fields)
|
|
47
|
+
defaults
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The source as turns read it (defaults + kits applied), for turn-time readers
|
|
51
|
+
# wired outside the graph (workflow triggers, persona evals). No settings store
|
|
52
|
+
# = nothing to apply, the source as is.
|
|
53
|
+
def for_turns(source, settings_store)
|
|
54
|
+
return source if settings_store.nil?
|
|
55
|
+
|
|
56
|
+
ProfileSource.new(Insika::ProfileSource.coerce(source), settings_store: settings_store)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The agent's own records behind a profile source: what authoring reads and
|
|
60
|
+
# writes. A plain source is its own.
|
|
61
|
+
def own(source) = source.is_a?(ProfileSource) ? source.source : source
|
|
62
|
+
|
|
63
|
+
# A ProfileSource that hands out profiles with the platform defaults and the
|
|
64
|
+
# agent's kits (Insika::Kits) applied.
|
|
65
|
+
# Read-only: writing a profile read here would save the defaults into the
|
|
66
|
+
# agent, so put/delete refuse; writers use #source (AgentDefaults.own).
|
|
67
|
+
# Other reads (all_raw) go to the wrapped source untouched.
|
|
68
|
+
class ProfileSource
|
|
69
|
+
include Insika::ProfileSource
|
|
70
|
+
|
|
71
|
+
attr_reader :source
|
|
72
|
+
|
|
73
|
+
def initialize(source, settings_store:)
|
|
74
|
+
@source = source
|
|
75
|
+
@settings_store = settings_store
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def fetch(id)
|
|
79
|
+
profile = @source.fetch(id)
|
|
80
|
+
profile && Insika::Kits.apply(fill(profile, normalized), kits)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def all
|
|
84
|
+
values = normalized
|
|
85
|
+
by_name = kits
|
|
86
|
+
@source.all.map { |profile| Insika::Kits.apply(fill(profile, values), by_name) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def ids = @source.ids
|
|
90
|
+
|
|
91
|
+
def put(*) = raise(Insika::Error, "agent profiles are written through their own source (AgentDefaults.own)")
|
|
92
|
+
def delete(*) = put
|
|
93
|
+
|
|
94
|
+
def respond_to_missing?(name, include_private = false) = @source.respond_to?(name, include_private) || super
|
|
95
|
+
|
|
96
|
+
def method_missing(name, ...)
|
|
97
|
+
return super unless @source.respond_to?(name)
|
|
98
|
+
|
|
99
|
+
@source.public_send(name, ...)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
# Settings["kits"] — applied after the defaults (Insika::Kits).
|
|
105
|
+
def kits = @settings_store.get["kits"] || {}
|
|
106
|
+
|
|
107
|
+
# Every inheritable field is normalized on its own by AgentProfile.build,
|
|
108
|
+
# so a profile takes the normalized values as they are (Data#with) and is
|
|
109
|
+
# not rebuilt per read.
|
|
110
|
+
def fill(profile, values)
|
|
111
|
+
missing = values.select { |field, _| profile.public_send(field).nil? }
|
|
112
|
+
missing.empty? ? profile : profile.with(**missing)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The defaults as AgentProfile.build normalizes them, rebuilt only when the
|
|
116
|
+
# stored defaults change. Frozen: every profile shares them. A field that
|
|
117
|
+
# does not build is left out (warned once per change), so one bad default
|
|
118
|
+
# cannot stop every turn.
|
|
119
|
+
def normalized
|
|
120
|
+
raw = @settings_store.get["agent_defaults"] || {}
|
|
121
|
+
digest = raw.hash
|
|
122
|
+
return @normalized if digest == @normalized_digest
|
|
123
|
+
|
|
124
|
+
@normalized = build_values(raw.slice(*FIELDS).compact)
|
|
125
|
+
@normalized_digest = digest
|
|
126
|
+
@normalized
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def build_values(raw)
|
|
130
|
+
raw.filter_map do |field, value|
|
|
131
|
+
built = AgentProfile.build(id: "agent-defaults", model: nil, field.to_sym => value)
|
|
132
|
+
[field.to_sym, Ractor.make_shareable(built.public_send(field))]
|
|
133
|
+
rescue Insika::ValidationError => e
|
|
134
|
+
warn "[insika] agent defaults: #{field} not applied: #{e.message}"
|
|
135
|
+
nil
|
|
136
|
+
end.to_h
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -31,6 +31,13 @@ module Insika
|
|
|
31
31
|
entry(agent_id, filename.to_s)&.fetch("content", nil)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# -> { name => content } every file of the agent, from ONE read of its record
|
|
35
|
+
# (which also carries the version history). Callers that need several files
|
|
36
|
+
# use this instead of `read` per file.
|
|
37
|
+
def contents(agent_id)
|
|
38
|
+
files(agent_id).transform_values { |entry| entry["content"] }
|
|
39
|
+
end
|
|
40
|
+
|
|
34
41
|
# -> [String] the agent's file names, lexicographic order.
|
|
35
42
|
def list(agent_id)
|
|
36
43
|
files(agent_id).keys.sort
|
|
@@ -88,6 +95,11 @@ module Insika
|
|
|
88
95
|
entry(agent_id, filename.to_s)&.fetch("history", []) || []
|
|
89
96
|
end
|
|
90
97
|
|
|
98
|
+
# -> String | nil: when the current content was written (iso8601).
|
|
99
|
+
def updated_at(agent_id, filename)
|
|
100
|
+
entry(agent_id, filename.to_s)&.fetch("updated_at", nil)
|
|
101
|
+
end
|
|
102
|
+
|
|
91
103
|
# Restores version `index` from history as the current content (a new write:
|
|
92
104
|
# the current one goes to the top of history). -> Hash (entry) | raises if index
|
|
93
105
|
# invalid / file nonexistent.
|
data/lib/insika/agent_profile.rb
CHANGED
|
@@ -35,12 +35,17 @@ module Insika
|
|
|
35
35
|
# a per-skill flag forced one decision onto every
|
|
36
36
|
# allowlist holding the skill. NOT `Allowlist`
|
|
37
37
|
# semantics — nil means NONE here (SkillCatalog#eager_for).
|
|
38
|
+
:kits, # kit names (Insika::Kits): bundles of skills/tools applied
|
|
39
|
+
# at READ time by AgentDefaults::ProfileSource — never
|
|
40
|
+
# copied into the record. Always a [String] ([] = none).
|
|
38
41
|
:context_providers, # provider allowlist
|
|
39
42
|
:workflows_allow, # applied by WorkflowAllowlist
|
|
40
43
|
:policies, # names in the Policy Registry
|
|
41
44
|
:prompt_refs, # names in the Prompt Catalog
|
|
42
45
|
:limits, # timeouts/budgets
|
|
43
46
|
:approvals_required, # tools that require approval (ApprovalRequired)
|
|
47
|
+
:customer_confirm, # tools the CUSTOMER confirms on the next message: the
|
|
48
|
+
# call is held, the turn ends with a question (nil = none)
|
|
44
49
|
:capabilities, # intents the agent can trigger.
|
|
45
50
|
# nil = NONE (opt-in, see above).
|
|
46
51
|
:subagents, # allowlist of child agent ids this agent MAY spawn
|
|
@@ -51,19 +56,23 @@ module Insika
|
|
|
51
56
|
:tools_deferred, # searchable-not-wired tools (Tool Search).
|
|
52
57
|
# nil = no deferred (all eager — parity);
|
|
53
58
|
# [names] ⊆ allowed_tools, exposed via tool_search.
|
|
59
|
+
:shared_conversations, # opt-in authoritative conversation persistence
|
|
54
60
|
:memory, # cross-session memory.
|
|
55
61
|
# nil/false = OFF (parity: provider []; the `remember`
|
|
56
62
|
# tool not wired); true = ON. Same opt-in as capabilities.
|
|
63
|
+
:memory_retrieval, # optional reranked context selection; nil = legacy memory block.
|
|
57
64
|
:prompt_caching, # Anthropic prompt caching (R3): nil/false = OFF
|
|
58
65
|
# (parity); true = ON. Same opt-in as `memory`. When ON
|
|
59
66
|
# AND the resolved provider is Anthropic, ChatBuilder sets
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
+
# the cache breakpoint at the end of the IDENTITY layer
|
|
68
|
+
# of the system (prompt, skills, tool index); memory,
|
|
69
|
+
# knowledge and briefing render below it as a second
|
|
70
|
+
# block, so a per-turn change there never re-writes the
|
|
71
|
+
# cached prefix. Immune to history eviction too
|
|
72
|
+
# (tools->system->messages prefix order). What still
|
|
73
|
+
# costs a WRITE every turn: a provider declaring
|
|
74
|
+
# `layer :identity` while emitting per-turn bytes —
|
|
75
|
+
# the doctor's cache-layers check flags it.
|
|
67
76
|
:tool_persistence, # the engine's "Tool discipline" block in the system
|
|
68
77
|
# prompt (retry weak/empty tool results with a different
|
|
69
78
|
# approach before giving up). THE ONE OPT-OUT FIELD:
|
|
@@ -81,9 +90,22 @@ module Insika
|
|
|
81
90
|
# THE MODEL SEES: an older full result is only the first
|
|
82
91
|
# occurrence; a model that wants an older detail re-calls
|
|
83
92
|
# the tool. Cheap half of compaction for bloated histories.
|
|
93
|
+
:fencing, # third-party text is sanitized before the model reads
|
|
94
|
+
# it: nil/false = OFF (parity — bytes reach the model
|
|
95
|
+
# as-is); true = ON. Same opt-in as `memory`. When ON,
|
|
96
|
+
# every tool result (after the evidence reshape), every
|
|
97
|
+
# <memory> fact/note and every <knowledge> concept pass
|
|
98
|
+
# Insika::Fence (NFKC, invisible/control characters out,
|
|
99
|
+
# transcript- and tool-call-shaped tags removed, forged
|
|
100
|
+
# turn markers defused, per-leaf cap), and the FenceNotice
|
|
101
|
+
# sentence rides under the identity. Default OFF this
|
|
102
|
+
# release: the goldens were baselined on unfenced bytes.
|
|
84
103
|
:params, # LLM generation params: a Hash with
|
|
85
104
|
# temperature/max_tokens/thinking, applied to the chat at
|
|
86
|
-
# stage 5. {} = provider defaults (parity).
|
|
105
|
+
# stage 5. {} = provider defaults (parity). Plus
|
|
106
|
+
# `provider_routing` — which UPSTREAM serves the model, for
|
|
107
|
+
# a gateway that has several; emitted under the wire key
|
|
108
|
+
# `provider` (OpenRouter's shape).
|
|
87
109
|
:budget, # spend caps per (tenant, agent) over
|
|
88
110
|
# CALENDAR windows (WS2): { "daily" => int,
|
|
89
111
|
# "monthly" => int, "soft" => bool, "alert_at" => 0.8 }.
|
|
@@ -318,11 +340,12 @@ module Insika
|
|
|
318
340
|
# platform `default_model` (Settings) at turn start via the ModelResolver.
|
|
319
341
|
def self.build(id:, model: nil, provider: nil, base_prompt: "", prompt_files: [],
|
|
320
342
|
tools_allow: nil, tools_deny: [], tools_allow_groups: nil, skills: nil,
|
|
321
|
-
skills_eager: nil, context_providers: nil, workflows_allow: nil,
|
|
343
|
+
skills_eager: nil, kits: nil, context_providers: nil, workflows_allow: nil,
|
|
322
344
|
policies: [], prompt_refs: [], limits: {}, approvals_required: nil,
|
|
323
|
-
|
|
345
|
+
customer_confirm: nil,
|
|
346
|
+
capabilities: nil, subagents: nil, tools_deferred: nil, memory: nil, memory_retrieval: nil, shared_conversations: false,
|
|
324
347
|
prompt_caching: nil, tool_persistence: nil, tool_output_compression: nil,
|
|
325
|
-
|
|
348
|
+
fencing: nil, params: {}, model_policy: nil, guardrails: nil, sandbox: nil,
|
|
326
349
|
refinement: nil, capabilities_declared: nil, edge_stream: nil, metadata: {},
|
|
327
350
|
budget: nil, reliability: nil, alerts: nil, routes: nil, stuck_signal: nil,
|
|
328
351
|
outputs: nil, stt_prompt: nil, briefing_fields: nil, grounding: nil, funnel: nil,
|
|
@@ -332,18 +355,21 @@ outputs: nil, stt_prompt: nil, briefing_fields: nil, grounding: nil, funnel: nil
|
|
|
332
355
|
prompt_files: Array(prompt_files), tools_allow: tools_allow,
|
|
333
356
|
tools_deny: Array(tools_deny), tools_allow_groups: tools_allow_groups, skills: skills,
|
|
334
357
|
skills_eager: skills_eager,
|
|
358
|
+
kits: Array(kits).map(&:to_s).uniq,
|
|
335
359
|
context_providers: context_providers, workflows_allow: workflows_allow,
|
|
336
360
|
policies: normalize_policies(policies, tools_allow: tools_allow, tools_deny: tools_deny,
|
|
337
361
|
tools_allow_groups: tools_allow_groups),
|
|
338
362
|
prompt_refs: Array(prompt_refs),
|
|
339
363
|
limits: DEFAULT_LIMITS.merge(limits), approvals_required: approvals_required,
|
|
364
|
+
customer_confirm: normalize_customer_confirm(customer_confirm, approvals_required),
|
|
340
365
|
capabilities: capabilities,
|
|
341
366
|
# opt-in like capabilities: nil => NONE. Array-normalize a present value so
|
|
342
367
|
# readers get a clean [] and the ChatBuilder gate (present? => wire) is stable.
|
|
343
368
|
subagents: subagents.nil? ? nil : Array(subagents).map(&:to_s),
|
|
344
|
-
tools_deferred: tools_deferred, memory: memory,
|
|
369
|
+
tools_deferred: tools_deferred, memory: memory, shared_conversations: shared_conversations == true,
|
|
370
|
+
memory_retrieval: normalize_memory_retrieval(memory_retrieval),
|
|
345
371
|
prompt_caching: prompt_caching, tool_persistence: tool_persistence,
|
|
346
|
-
tool_output_compression: tool_output_compression,
|
|
372
|
+
tool_output_compression: tool_output_compression, fencing: fencing,
|
|
347
373
|
# The free-form hashes arrive with symbol keys (internal build) OR string
|
|
348
374
|
# keys (StoredProfileSource JSON round-trip). Normalize to string keys ONCE
|
|
349
375
|
# here — the single front door every profile passes through — so no reader
|
|
@@ -390,10 +416,9 @@ outputs: nil, stt_prompt: nil, briefing_fields: nil, grounding: nil, funnel: nil
|
|
|
390
416
|
# free-form hashes; shape-validated by the command/engine/doctor
|
|
391
417
|
# (never here — the refinement precedent). nil = off (parity).
|
|
392
418
|
harvest: Coercion.deep_stringify(harvest),
|
|
393
|
-
# knowledge is profile DATA
|
|
394
|
-
#
|
|
395
|
-
|
|
396
|
-
knowledge: Coercion.deep_stringify(knowledge),
|
|
419
|
+
# knowledge is profile DATA. Legacy extraction remains free-form;
|
|
420
|
+
# an explicit rerank declaration is validated when loaded.
|
|
421
|
+
knowledge: normalize_knowledge(knowledge),
|
|
397
422
|
# schedules is profile DATA, deep-stringified like the other
|
|
398
423
|
# free-form hashes (an ARRAY of declarations); parsed into
|
|
399
424
|
# Insika::Schedule entries by the engine/doctor/Studio (shape-validated
|
|
@@ -402,6 +427,46 @@ outputs: nil, stt_prompt: nil, briefing_fields: nil, grounding: nil, funnel: nil
|
|
|
402
427
|
)
|
|
403
428
|
end
|
|
404
429
|
|
|
430
|
+
def self.normalize_knowledge(value)
|
|
431
|
+
config = Coercion.deep_stringify(value)
|
|
432
|
+
return config unless config.is_a?(Hash) && config.key?("rerank")
|
|
433
|
+
|
|
434
|
+
validate_rerank(config, "knowledge")
|
|
435
|
+
config
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def self.normalize_memory_retrieval(value)
|
|
439
|
+
return nil if value.nil?
|
|
440
|
+
|
|
441
|
+
config = Coercion.deep_stringify(value)
|
|
442
|
+
raise ValidationError, "memory_retrieval must be an object" unless config.is_a?(Hash)
|
|
443
|
+
|
|
444
|
+
validate_rerank(config, "memory_retrieval", require_top_k: true)
|
|
445
|
+
config
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def self.validate_rerank(config, field, require_top_k: false)
|
|
449
|
+
rerank = config["rerank"]
|
|
450
|
+
raise ValidationError, "#{field}.rerank must be an object" unless rerank.is_a?(Hash)
|
|
451
|
+
|
|
452
|
+
top_k = require_top_k ? config["top_k"] : config.fetch("top_k", 5)
|
|
453
|
+
limit = rerank["candidate_limit"]
|
|
454
|
+
timeout = rerank["timeout_seconds"]
|
|
455
|
+
valid = top_k.is_a?(Integer) && top_k.positive? &&
|
|
456
|
+
limit.is_a?(Integer) && limit >= top_k && limit <= 100 &&
|
|
457
|
+
timeout.is_a?(Numeric) && timeout.finite? && timeout.positive? &&
|
|
458
|
+
%w[provider model].all? { |key| rerank[key].is_a?(String) && !rerank[key].strip.empty? }
|
|
459
|
+
raise ValidationError, "invalid #{field}.rerank configuration" unless valid
|
|
460
|
+
|
|
461
|
+
require "ruby_llm"
|
|
462
|
+
begin
|
|
463
|
+
model = RubyLLM.models.find(rerank["model"], provider: rerank["provider"])
|
|
464
|
+
rescue RubyLLM::ModelNotFoundError
|
|
465
|
+
raise ValidationError, "#{field}.rerank model is not registered for provider"
|
|
466
|
+
end
|
|
467
|
+
raise ValidationError, "#{field}.rerank model does not support reranking" unless model.type == :rerank
|
|
468
|
+
end
|
|
469
|
+
|
|
405
470
|
# Declaring a tool allow/deny list IS opting into it. The list is only ever
|
|
406
471
|
# applied by the builtin `tool_allowlist` policy, and the Policy::Engine runs
|
|
407
472
|
# ONLY the policies a profile names — so a profile with `tools_allow: [a, b]`
|
|
@@ -449,6 +514,23 @@ outputs: nil, stt_prompt: nil, briefing_fields: nil, grounding: nil, funnel: nil
|
|
|
449
514
|
names
|
|
450
515
|
end
|
|
451
516
|
|
|
517
|
+
# nil/empty -> nil (off); else [String], trimmed, uniq. A tool is confirmed by
|
|
518
|
+
# the customer OR approved by the operator, never both: the two hold a call in
|
|
519
|
+
# opposite ways (the turn ends with a question vs. the turn suspends), and one
|
|
520
|
+
# tool cannot do both at once.
|
|
521
|
+
def self.normalize_customer_confirm(list, approvals)
|
|
522
|
+
names = Array(list).map { |n| n.to_s.strip }.reject(&:empty?).uniq
|
|
523
|
+
return nil if names.empty?
|
|
524
|
+
|
|
525
|
+
both = names & Array(approvals).map(&:to_s)
|
|
526
|
+
unless both.empty?
|
|
527
|
+
raise Insika::ValidationError,
|
|
528
|
+
"customer_confirm and approvals_required overlap (#{both.join(', ')}): a tool is " \
|
|
529
|
+
"confirmed by the customer or approved by the operator, not both"
|
|
530
|
+
end
|
|
531
|
+
names
|
|
532
|
+
end
|
|
533
|
+
|
|
452
534
|
# opt-in for an optional tool = being in the agent's allow list.
|
|
453
535
|
def tool_opted_in?(name)
|
|
454
536
|
Array(tools_allow).include?(name)
|
|
@@ -22,6 +22,17 @@ module Insika
|
|
|
22
22
|
AUTHENTICATED_PATH = "/studio/artifacts/%{id}/content"
|
|
23
23
|
SIGNED_PATH = "/studio/artifacts/s/%{id}?exp=%{exp}&sig=%{sig}"
|
|
24
24
|
|
|
25
|
+
# The Studio's signing config, read from the environment.
|
|
26
|
+
# -> { key:, ttl:, base_url: } | nil (no signing key -> no signed surface).
|
|
27
|
+
def studio_config
|
|
28
|
+
key = Insika::EnvSchema.read("INSIKA_ARTIFACT_SIGNING_KEY")
|
|
29
|
+
return nil if key.to_s.empty?
|
|
30
|
+
|
|
31
|
+
{ key: key,
|
|
32
|
+
ttl: (Insika::EnvSchema.read("INSIKA_ARTIFACT_SIGNING_TTL") || 604_800).to_i,
|
|
33
|
+
base_url: Insika::Coercion.presence(Insika::EnvSchema.read("INSIKA_PUBLIC_URL")).to_s }
|
|
34
|
+
end
|
|
35
|
+
|
|
25
36
|
# -> hex token (64 chars) | nil when the key is blank (no signed surface).
|
|
26
37
|
def sign(id:, expires_at:, key:)
|
|
27
38
|
key = key.to_s
|
|
@@ -158,6 +158,11 @@
|
|
|
158
158
|
log.scrollTop = log.scrollHeight;
|
|
159
159
|
} else if (event === "working" && !bubbleEl && !note) {
|
|
160
160
|
note = say("note", "working…");
|
|
161
|
+
} else if (event === "ui" && data.items && data.items.length) {
|
|
162
|
+
// A presentation tool picked cards: a plain list of caption + link.
|
|
163
|
+
// The host page restyles or replaces this; the protocol is the frame.
|
|
164
|
+
if (note) { note.remove(); note = null; }
|
|
165
|
+
show(data);
|
|
161
166
|
} else if (event === "error") {
|
|
162
167
|
if (note) { note.remove(); note = null; }
|
|
163
168
|
throw new Error(data.message || "something went wrong");
|
|
@@ -257,6 +262,34 @@
|
|
|
257
262
|
|
|
258
263
|
// --- helpers --------------------------------------------------------
|
|
259
264
|
|
|
265
|
+
function show(ui) {
|
|
266
|
+
var list = el("ul", "insika-m ui");
|
|
267
|
+
if (ui.title) {
|
|
268
|
+
var head = el("li", "title");
|
|
269
|
+
head.textContent = ui.title;
|
|
270
|
+
list.appendChild(head);
|
|
271
|
+
}
|
|
272
|
+
ui.items.forEach(function (item) {
|
|
273
|
+
var li = el("li", "");
|
|
274
|
+
var label = item.caption || item.id || item.url;
|
|
275
|
+
// Only http(s) becomes a link: the url is backend data, and a
|
|
276
|
+
// `javascript:` or `data:` scheme must never be one click away.
|
|
277
|
+
if (/^https?:\/\//i.test(item.url || "")) {
|
|
278
|
+
var a = el("a", "");
|
|
279
|
+
a.href = item.url;
|
|
280
|
+
a.target = "_blank";
|
|
281
|
+
a.rel = "noopener";
|
|
282
|
+
a.textContent = label;
|
|
283
|
+
li.appendChild(a);
|
|
284
|
+
} else {
|
|
285
|
+
li.textContent = label;
|
|
286
|
+
}
|
|
287
|
+
list.appendChild(li);
|
|
288
|
+
});
|
|
289
|
+
log.appendChild(list);
|
|
290
|
+
log.scrollTop = log.scrollHeight;
|
|
291
|
+
}
|
|
292
|
+
|
|
260
293
|
function say(kind, content) {
|
|
261
294
|
var node = el("div", "insika-m " + kind);
|
|
262
295
|
node.textContent = content;
|
data/lib/insika/channels/web.rb
CHANGED
|
@@ -155,8 +155,9 @@ module Insika
|
|
|
155
155
|
# path — `POST /messages` with an unknown id is a 404, not a new conversation.
|
|
156
156
|
def mint_session_id = "#{@id}:#{SecureRandom.hex(16)}"
|
|
157
157
|
|
|
158
|
-
# Turn Event -> SSE frame | nil.
|
|
159
|
-
# protocol: what to type, what to say while a tool runs,
|
|
158
|
+
# Turn Event -> SSE frame | nil. Five frames, which is the whole widget
|
|
159
|
+
# protocol: what to type, what to say while a tool runs, what to show
|
|
160
|
+
# (a presentation tool's cards), and how it ended.
|
|
160
161
|
#
|
|
161
162
|
# `:intermediate` and `:thinking` are deliberately absent. `:content` is the
|
|
162
163
|
# ANSWER — the model's narration on the way there is internal, and a
|
|
@@ -165,6 +166,8 @@ module Insika
|
|
|
165
166
|
case event.type
|
|
166
167
|
when :content then sse("delta", { delta: event.data[:delta].to_s })
|
|
167
168
|
when :tool_call then sse("working", { name: event.data[:name].to_s })
|
|
169
|
+
when :ui then sse("ui", { component: event.data[:component].to_s, title: event.data[:title],
|
|
170
|
+
items: Array(event.data[:items]) })
|
|
168
171
|
when :task_completed then sse("done", {})
|
|
169
172
|
when :task_failed then sse("error", { message: event.data[:message].to_s })
|
|
170
173
|
when :task_cancelled then sse("error", { message: "task cancelled" })
|