insika 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +69 -3
- data/README.md +1 -1
- data/bin/insika +22 -7
- data/docs/AGENTS.md +129 -5
- data/docs/CHANNELS.md +1 -1
- data/docs/CONTEXT.md +22 -5
- data/docs/DEPLOY.md +30 -10
- data/docs/EMBEDDING.md +11 -7
- data/docs/EVALS.md +1 -1
- data/docs/LOADTEST.md +3 -2
- data/docs/OBSERVABILITY.md +11 -2
- data/docs/REFINEMENT.md +6 -6
- data/docs/RELEASING.md +7 -7
- data/docs/RUNNING-LOCAL.md +1 -1
- data/docs/SECURITY.md +24 -11
- data/docs/SKILLS.md +189 -3
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/index.md +1 -1
- data/docs/onboarding/start.md +1 -1
- data/lib/insika/agent_profile.rb +89 -22
- data/lib/insika/alert_dispatcher.rb +139 -0
- data/lib/insika/baseline_store.rb +2 -2
- data/lib/insika/budget_ledger.rb +135 -0
- data/lib/insika/channel_delivery.rb +14 -11
- data/lib/insika/channel_registry.rb +1 -1
- data/lib/insika/channels/relay.rb +3 -3
- data/lib/insika/channels/web/widget.js +2 -2
- data/lib/insika/channels/web.rb +7 -7
- data/lib/insika/channels/webhook.rb +58 -0
- data/lib/insika/chat_builder.rb +62 -13
- data/lib/insika/circuit_state.rb +114 -0
- data/lib/insika/coercion.rb +8 -0
- data/lib/insika/commands/agent_payload.rb +5 -3
- data/lib/insika/commands/create_agent.rb +2 -2
- data/lib/insika/commands/create_session.rb +1 -1
- data/lib/insika/commands/delete_llm_provider.rb +1 -1
- data/lib/insika/commands/delete_skill.rb +43 -0
- data/lib/insika/commands/gate_refinement.rb +12 -12
- data/lib/insika/commands/import_mcp_tools.rb +1 -1
- data/lib/insika/commands/import_tools.rb +4 -4
- data/lib/insika/commands/issue_tenant_token.rb +41 -0
- data/lib/insika/commands/resolve_refinement.rb +1 -1
- data/lib/insika/commands/revoke_token.rb +39 -0
- data/lib/insika/commands/rotate_tenant_token.rb +43 -0
- data/lib/insika/commands/run_refinement.rb +5 -5
- data/lib/insika/commands/send_message.rb +9 -9
- data/lib/insika/commands/set_agent_tools.rb +1 -1
- data/lib/insika/commands/set_skill_agents.rb +60 -19
- data/lib/insika/commands/trigger_workflow.rb +1 -1
- data/lib/insika/commands/update_agent.rb +1 -1
- data/lib/insika/commands/write_data_tool.rb +1 -1
- data/lib/insika/commands/write_golden.rb +1 -1
- data/lib/insika/commands/write_skill.rb +19 -9
- data/lib/insika/config_store.rb +8 -4
- data/lib/insika/context/builder.rb +2 -2
- data/lib/insika/context/fragment.rb +27 -3
- data/lib/insika/context/priority.rb +3 -2
- data/lib/insika/context/providers/memory.rb +1 -1
- data/lib/insika/context/providers/request.rb +1 -1
- data/lib/insika/context/providers/session.rb +17 -2
- data/lib/insika/context/providers/skill.rb +5 -1
- data/lib/insika/context/providers/skill_trigger.rb +128 -0
- data/lib/insika/context_trace_store.rb +92 -0
- data/lib/insika/delegation_store.rb +2 -2
- data/lib/insika/doctor.rb +250 -5
- data/lib/insika/dsl/runtime.rb +12 -9
- data/lib/insika/dsl/server_boot.rb +4 -3
- data/lib/insika/dsl/system.rb +1 -1
- data/lib/insika/dsl.rb +72 -15
- data/lib/insika/edge_limiter.rb +144 -6
- data/lib/insika/egress_guard.rb +3 -3
- data/lib/insika/env_schema.rb +13 -10
- data/lib/insika/errors.rb +61 -5
- data/lib/insika/evals/assertions.rb +12 -12
- data/lib/insika/evals/baseline.rb +3 -3
- data/lib/insika/evals/golden.rb +8 -8
- data/lib/insika/evals/judge.rb +7 -7
- data/lib/insika/evals/pairwise.rb +3 -3
- data/lib/insika/evals/report.rb +2 -2
- data/lib/insika/evals/runner.rb +6 -6
- data/lib/insika/evals/transport.rb +2 -2
- data/lib/insika/event_stream.rb +23 -5
- data/lib/insika/executor.rb +423 -108
- data/lib/insika/frontmatter.rb +1 -1
- data/lib/insika/golden_store.rb +2 -2
- data/lib/insika/http_client.rb +3 -3
- data/lib/insika/inbound_log.rb +1 -1
- data/lib/insika/llm_configurator.rb +3 -3
- data/lib/insika/loop_detector.rb +143 -0
- data/lib/insika/mcp_http_client.rb +4 -4
- data/lib/insika/mcp_tool_ingestor.rb +6 -6
- data/lib/insika/message_origin.rb +2 -2
- data/lib/insika/model_resolver.rb +1 -1
- data/lib/insika/model_selection.rb +5 -4
- data/lib/insika/onboarding.rb +2 -2
- data/lib/insika/outbox_store.rb +2 -2
- data/lib/insika/overlay_tool_registry.rb +3 -4
- data/lib/insika/pack.rb +3 -3
- data/lib/insika/pack_importer.rb +17 -15
- data/lib/insika/pending_action_store.rb +1 -1
- data/lib/insika/plugin/loader.rb +2 -2
- data/lib/insika/policy/policy.rb +1 -1
- data/lib/insika/profile_source.rb +12 -6
- data/lib/insika/provider_error_classifier.rb +160 -0
- data/lib/insika/queue_policy.rb +2 -2
- data/lib/insika/recovery.rb +47 -6
- data/lib/insika/refinement/candidate.rb +4 -4
- data/lib/insika/refinement/evidence_collector.rb +6 -6
- data/lib/insika/refinement/gate.rb +7 -7
- data/lib/insika/refinement/panel.rb +7 -7
- data/lib/insika/refinement/proposer.rb +9 -9
- data/lib/insika/refinement_store.rb +12 -12
- data/lib/insika/reliability.rb +185 -0
- data/lib/insika/safety/config.rb +2 -2
- data/lib/insika/safety/detectors.rb +5 -5
- data/lib/insika/safety/factory.rb +3 -3
- data/lib/insika/safety/input_guardrail.rb +19 -4
- data/lib/insika/safety/moderator.rb +19 -11
- data/lib/insika/safety/output_filter.rb +2 -2
- data/lib/insika/safety/output_validator.rb +2 -2
- data/lib/insika/safety/safe_responses.rb +1 -1
- data/lib/insika/sandbox/boundary.rb +2 -2
- data/lib/insika/sandbox.rb +1 -1
- data/lib/insika/server/app.rb +223 -51
- data/lib/insika/server/boot.rb +4 -4
- data/lib/insika/server/rack_app.rb +15 -7
- data/lib/insika/server/responses.rb +18 -8
- data/lib/insika/server/tenant_auth.rb +61 -0
- data/lib/insika/session_actor.rb +3 -3
- data/lib/insika/session_store.rb +1 -1
- data/lib/insika/settings_store.rb +5 -5
- data/lib/insika/shutdown.rb +4 -4
- data/lib/insika/skill_catalog.rb +127 -20
- data/lib/insika/skill_store.rb +70 -22
- data/lib/insika/steer_injector.rb +1 -1
- data/lib/insika/store.rb +1 -1
- data/lib/insika/studio/app.rb +183 -61
- data/lib/insika/studio/assets/dist/application.js +25 -24
- data/lib/insika/studio/forms.rb +13 -18
- data/lib/insika/studio/nav_icons.rb +1 -1
- data/lib/insika/studio/views/_message.erb +2 -2
- data/lib/insika/studio/views/agent_detail.erb +2 -2
- data/lib/insika/studio/views/agents.erb +1 -1
- data/lib/insika/studio/views/refinement.erb +4 -4
- data/lib/insika/studio/views/session.erb +78 -3
- data/lib/insika/studio/views/settings.erb +7 -12
- data/lib/insika/studio/views/skills.erb +67 -12
- data/lib/insika/subagent_graph.rb +3 -3
- data/lib/insika/task_actor.rb +3 -3
- data/lib/insika/task_store.rb +1 -1
- data/lib/insika/telemetry/pricing.rb +3 -3
- data/lib/insika/telemetry/recorder.rb +1 -1
- data/lib/insika/telemetry.rb +2 -2
- data/lib/insika/testing/store_contract.rb +27 -27
- data/lib/insika/tick.rb +122 -0
- data/lib/insika/token_store.rb +168 -0
- data/lib/insika/tool_assembly.rb +5 -5
- data/lib/insika/tool_definition.rb +8 -8
- data/lib/insika/tool_envelope.rb +1 -1
- data/lib/insika/tool_manifest.rb +6 -6
- data/lib/insika/tool_output_compressor.rb +100 -0
- data/lib/insika/tool_store.rb +1 -1
- data/lib/insika/tool_trace_store.rb +1 -1
- data/lib/insika/tools/concurrency.rb +2 -2
- data/lib/insika/tools/data_defined_tool.rb +4 -5
- data/lib/insika/tools/load_skill.rb +61 -3
- data/lib/insika/tools/stuck_signal.rb +44 -0
- data/lib/insika/tools/subagent.rb +4 -4
- data/lib/insika/tools/subagents.rb +1 -1
- data/lib/insika/turn_output.rb +2 -2
- data/lib/insika/turn_state.rb +17 -13
- data/lib/insika/turn_timing.rb +2 -2
- data/lib/insika/usage_ledger.rb +1 -1
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +77 -26
- data/lib/insika/workflow.rb +1 -1
- data/lib/insika/workflow_registry.rb +1 -1
- data/lib/insika.rb +32 -15
- metadata +19 -2
- data/lib/insika/server/admin_auth.rb +0 -29
data/lib/insika/frontmatter.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Insika
|
|
|
7
7
|
# The convention is YAML, but real packs carry PROSE in `description` — with `: `
|
|
8
8
|
# (colon + space), quotes, parentheses — which STRICT YAML rejects
|
|
9
9
|
# ("mapping values are not allowed in this context"). The OpenClaw gateway
|
|
10
|
-
# tolerates it; the insika has to tolerate it too (
|
|
10
|
+
# tolerates it; the insika has to tolerate it too (the same pack must hold).
|
|
11
11
|
#
|
|
12
12
|
# Strategy: try YAML (respects quoted / multi-line / lists); if the YAML
|
|
13
13
|
# fails OR doesn't yield a Hash, fall back to a LINE-BY-LINE parse that splits on
|
data/lib/insika/golden_store.rb
CHANGED
|
@@ -5,7 +5,7 @@ require "time"
|
|
|
5
5
|
require "yaml"
|
|
6
6
|
|
|
7
7
|
module Insika
|
|
8
|
-
# AUTHORED eval cases (
|
|
8
|
+
# AUTHORED eval cases (promoted to a store by).
|
|
9
9
|
#
|
|
10
10
|
# A golden case used to be a YAML file in `evals/golden/`, which means only someone
|
|
11
11
|
# with a checkout and a text editor could add one. The rubric is the part of an eval
|
|
@@ -133,7 +133,7 @@ module Insika
|
|
|
133
133
|
def case_hash(golden)
|
|
134
134
|
h = { "id" => golden.id, "agent" => golden.agent, "turns" => golden.turns }
|
|
135
135
|
h["requires"] = golden.requires unless golden.requires.empty?
|
|
136
|
-
# Same rule for `reference
|
|
136
|
+
# Same rule for `reference`: omitted when absent, never dropped
|
|
137
137
|
# when present. A case that lost its reference in a round-trip would stop being
|
|
138
138
|
# compared against the incumbent and the report would look identical.
|
|
139
139
|
h["reference"] = golden.reference unless golden.reference.empty?
|
data/lib/insika/http_client.rb
CHANGED
|
@@ -5,11 +5,11 @@ require "uri"
|
|
|
5
5
|
require_relative "coercion"
|
|
6
6
|
|
|
7
7
|
module Insika
|
|
8
|
-
# Default HTTP client for data-tools. Net::HTTP (stdlib, zero-dep — spec
|
|
8
|
+
# Default HTTP client for data-tools. Net::HTTP (stdlib, zero-dep — spec)
|
|
9
9
|
# with its own socket timeouts (mitigates reactor blocking even if the
|
|
10
|
-
# envelope timer doesn't fire) and a response-size CAP via streaming (
|
|
10
|
+
# envelope timer doesn't fire) and a response-size CAP via streaming (
|
|
11
11
|
# avoids OOM). It is INJECTABLE: tests pass a double (none hit the network);
|
|
12
|
-
#
|
|
12
|
+
# can swap in async-http without touching DataDefinedTool.
|
|
13
13
|
#
|
|
14
14
|
# Contract: request(method:, url:, headers:, body:, timeout:) -> { status:, body: }
|
|
15
15
|
# (+ `location:` on a 3xx). It does NOT follow redirects: the destination is
|
data/lib/insika/inbound_log.rb
CHANGED
|
@@ -4,7 +4,7 @@ require "time"
|
|
|
4
4
|
|
|
5
5
|
module Insika
|
|
6
6
|
# Short-lived memory of inbound event ids, so a platform's retry does not become
|
|
7
|
-
# a second LLM turn and a second reply
|
|
7
|
+
# a second LLM turn and a second reply.
|
|
8
8
|
#
|
|
9
9
|
# Every messaging platform retries a webhook it did not see acked in time, and a
|
|
10
10
|
# relay consumer that hands us its own queue does the same. Without this, the
|
|
@@ -12,7 +12,7 @@ module Insika
|
|
|
12
12
|
# matching accessor) does NOT blow up: it goes into `skipped` (degrades to "restart
|
|
13
13
|
# recommended", like OpenClaw), the rest applies.
|
|
14
14
|
#
|
|
15
|
-
# ⚠️ GOTCHA — the DEFAULT target is a global singleton (RubyLLM research
|
|
15
|
+
# ⚠️ GOTCHA — the DEFAULT target is a global singleton (RubyLLM research).
|
|
16
16
|
# With no `configure:`, `apply`/`unapply` mutate the PROCESS-WIDE config
|
|
17
17
|
# (`RubyLLM.config`). They are therefore admin operations (rare, operator-driven:
|
|
18
18
|
# a provider key/base edit in the Studio), NOT a per-request/per-turn path — a
|
|
@@ -22,7 +22,7 @@ module Insika
|
|
|
22
22
|
# MODEL per turn — the model is chosen at chat build time (ModelResolver ->
|
|
23
23
|
# chat(model:)), never by mutating the global.
|
|
24
24
|
#
|
|
25
|
-
# PER-GRAPH credentials are no longer hypothetical
|
|
25
|
+
# PER-GRAPH credentials are no longer hypothetical: the DSL runtime
|
|
26
26
|
# passes `configure:` targeting its own `RubyLLM.context` — an isolated config dup
|
|
27
27
|
# — so an operator's key edit in an EMBEDDED graph applies to that graph and stops
|
|
28
28
|
# there. See `DSL::Runtime#llm_configure` and docs/EMBEDDING.md.
|
|
@@ -62,7 +62,7 @@ module Insika
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
# UNDOES a provider's config in RubyLLM at runtime (delete without a restart,
|
|
65
|
-
#
|
|
65
|
+
# clears `<api>_api_key`/`<api>_api_base`. A provider that RubyLLM
|
|
66
66
|
# doesn't recognize (no accessor) -> unapplied: false (nothing applied, nothing to undo).
|
|
67
67
|
# -> { unapplied: bool }.
|
|
68
68
|
def unapply(api)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
# loop detection by (tool, args) hash, with a ONE-SHOT intervention.
|
|
5
|
+
#
|
|
6
|
+
# `max_tool_calls` bounds how MANY tool calls a turn makes, not how useful they
|
|
7
|
+
# are: a model retrying the exact same call — same tool, identical arguments —
|
|
8
|
+
# after an empty or error result burns the whole budget doing something that
|
|
9
|
+
# was settled on the first repeat. This detector is the engine saying so, once.
|
|
10
|
+
#
|
|
11
|
+
# The streak is CONSECUTIVE and turn-scoped, like the max_tool_calls counter it
|
|
12
|
+
# sits next to in ChatBuilder#wire_callbacks: a call revisited much later in a
|
|
13
|
+
# long turn is not the pathology being caught, and semantic ("nearly the same")
|
|
14
|
+
# matching is how a guard-rail starts eating legitimate retries.
|
|
15
|
+
#
|
|
16
|
+
# Two invariants, both borrowed from SteerInjector, because the
|
|
17
|
+
# intervention is a `user` message appended mid-loop:
|
|
18
|
+
#
|
|
19
|
+
# · **Batch boundary only.** The append happens after the LAST tool result of a
|
|
20
|
+
# batch closes — a `user` message between two tool results is rejected by
|
|
21
|
+
# Anthropic outright. Same arithmetic: an assistant message opens a batch of
|
|
22
|
+
# N, the Nth `role: tool` message closes it.
|
|
23
|
+
# · **A halted batch receives nothing.** With `halt_when` there is no next
|
|
24
|
+
# model step; a warning appended there would sit unanswered forever.
|
|
25
|
+
#
|
|
26
|
+
# The repeated call itself STILL RUNS — fabricating a synthetic result would
|
|
27
|
+
# teach the model that tools lie (the failure refuses). The
|
|
28
|
+
# warning rides after the truth; only a repeat that arrives AFTER the warning
|
|
29
|
+
# was spent aborts, through the existing TimeoutError(stage: :tool_limit) path.
|
|
30
|
+
class LoopDetector
|
|
31
|
+
# The one intervention text, verbatim — a fixed engine sentence, so a report
|
|
32
|
+
# can identify it without an origin stamp (chat messages carry none).
|
|
33
|
+
def self.intervention(name, streak)
|
|
34
|
+
"You have called `#{name}` with identical arguments #{streak} times in a row and " \
|
|
35
|
+
"received the same result every time. Repeating it will not produce new information. " \
|
|
36
|
+
"Do not call it again with the same arguments — answer with what you already have, " \
|
|
37
|
+
"or change your approach."
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# chat: the turn's chat — must answer #add_message (the boundary append).
|
|
41
|
+
# limit: the streak that triggers the intervention (profile's
|
|
42
|
+
# max_tool_repeat). Values < 2 mean OFF: a "streak of 1" is every
|
|
43
|
+
# call, which is meaningless.
|
|
44
|
+
# emit: ->(type, data) — the Executor's emitter, bound to the task.
|
|
45
|
+
def initialize(chat:, limit:, emit:)
|
|
46
|
+
@chat = chat
|
|
47
|
+
@limit = limit
|
|
48
|
+
@emit = emit
|
|
49
|
+
@last = nil # fingerprint of the previous call (nil = none yet)
|
|
50
|
+
@streak = 0
|
|
51
|
+
@intervened = false # the ONE warning of this turn has been delivered
|
|
52
|
+
@pending = false # detection fired; waiting for the batch boundary
|
|
53
|
+
@expected = nil # tool calls announced by the batch in flight
|
|
54
|
+
@seen = 0
|
|
55
|
+
@halted = false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# From ChatBuilder's before_tool_call. Raises BEFORE the call executes once
|
|
59
|
+
# the warning is spent — bounded spend is the point of aborting here.
|
|
60
|
+
def tool_call(name, arguments)
|
|
61
|
+
fingerprint = [name.to_s, canonical(arguments)]
|
|
62
|
+
if fingerprint == @last
|
|
63
|
+
@streak += 1
|
|
64
|
+
else
|
|
65
|
+
# A different call broke the run: the loop resolved itself, so a warning
|
|
66
|
+
# armed earlier is moot — it must not fire later naming the WRONG call.
|
|
67
|
+
@streak = 1
|
|
68
|
+
@pending = false
|
|
69
|
+
end
|
|
70
|
+
@last = fingerprint
|
|
71
|
+
return if @streak < @limit
|
|
72
|
+
|
|
73
|
+
if @intervened
|
|
74
|
+
raise Insika::TimeoutError.new(
|
|
75
|
+
"tool loop detected (#{name} repeated with identical arguments after a warning)",
|
|
76
|
+
stage: :tool_limit)
|
|
77
|
+
end
|
|
78
|
+
@pending = true
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# From ChatBuilder's after_tool_result, with the RAW result — the only place
|
|
82
|
+
# a Tool::Halt is still recognizable (SteerInjector's comment applies here).
|
|
83
|
+
def tool_result(result)
|
|
84
|
+
@halted = true if defined?(RubyLLM::Tool::Halt) && result.is_a?(RubyLLM::Tool::Halt)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# RubyLLM after_message. An assistant message carrying tool calls OPENS a
|
|
88
|
+
# batch; the Nth tool result CLOSES it — the one boundary where appending
|
|
89
|
+
# is valid.
|
|
90
|
+
def message_ended(message)
|
|
91
|
+
role = field(message, :role).to_s
|
|
92
|
+
return open_batch(message) if role == "assistant"
|
|
93
|
+
return unless role == "tool" && @expected
|
|
94
|
+
|
|
95
|
+
@seen += 1
|
|
96
|
+
intervene! if @seen >= @expected
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def open_batch(message)
|
|
102
|
+
calls = field(message, :tool_calls)
|
|
103
|
+
size = calls.respond_to?(:size) ? calls.size : 0
|
|
104
|
+
# No tool call = the model talking; the turn is ending and a pending
|
|
105
|
+
# warning is moot — the loop resolved itself.
|
|
106
|
+
return @expected = nil if size.zero?
|
|
107
|
+
|
|
108
|
+
@expected = size
|
|
109
|
+
@seen = 0
|
|
110
|
+
@halted = false
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def intervene!
|
|
114
|
+
@expected = nil
|
|
115
|
+
return unless @pending
|
|
116
|
+
@pending = false
|
|
117
|
+
return if @halted # nothing will read it (halt_when): drop, never deliver
|
|
118
|
+
|
|
119
|
+
@intervened = true
|
|
120
|
+
name, = @last
|
|
121
|
+
@chat.add_message(role: :user, content: self.class.intervention(name, @streak))
|
|
122
|
+
# Counts and the tool name, never the arguments — order numbers are PII.
|
|
123
|
+
@emit.call(:tool_loop_intervened, { name: name, streak: @streak })
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# (name, args) hash: symbols vs strings and key order must not split an
|
|
127
|
+
# identical call into two fingerprints. Compared with ==, never hashed.
|
|
128
|
+
def canonical(value)
|
|
129
|
+
case value
|
|
130
|
+
when Hash then value.map { |k, v| [k.to_s, canonical(v)] }.sort_by(&:first)
|
|
131
|
+
when Array then value.map { |v| canonical(v) }
|
|
132
|
+
else value
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def field(message, name)
|
|
137
|
+
return message.public_send(name) if message.respond_to?(name)
|
|
138
|
+
return message[name] || message[name.to_s] if message.respond_to?(:[])
|
|
139
|
+
|
|
140
|
+
nil
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
5
|
module Insika
|
|
6
|
-
# MINIMAL MCP client over HTTP JSON-RPC
|
|
6
|
+
# MINIMAL MCP client over HTTP JSON-RPC. Discovers the tools
|
|
7
7
|
# of an MCP instance with HTTP transport by making a JSON-RPC 2.0 `tools/list`
|
|
8
8
|
# POST to the instance endpoint, behind the EgressGuard (SSRF — the url comes
|
|
9
|
-
# from editable config
|
|
9
|
+
# from editable config). It is the DEFAULT client injected into the
|
|
10
10
|
# McpToolIngestor; tests pass a Fake (duck-typed) in its place.
|
|
11
11
|
#
|
|
12
12
|
# Contract (MCP client duck-type): `#list_tools -> [{name, description,
|
|
13
13
|
# inputSchema}]` — the same MCP envelope that the ToolManifest adapter normalizes.
|
|
14
14
|
#
|
|
15
|
-
# SCOPE (bounded
|
|
15
|
+
# SCOPE (bounded): only the minimal handshake of ONE stateless `tools/list`
|
|
16
16
|
# POST. Does NOT implement the full MCP session lifecycle (initialize/protocol
|
|
17
17
|
# negotiation/session-id/notifications) nor the stdio transport — that is the
|
|
18
|
-
# "real MCP transport", later work (out-of-scope, see spec
|
|
18
|
+
# "real MCP transport", later work (out-of-scope, see spec). It serves
|
|
19
19
|
# simple HTTP MCP servers (direct JSON-RPC) and proves the ingestion seam.
|
|
20
20
|
class McpHttpClient
|
|
21
21
|
JSONRPC_VERSION = "2.0"
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
5
|
module Insika
|
|
6
|
-
# LIVE MCP ingestion (
|
|
6
|
+
# LIVE MCP ingestion (/ spec): discovers the tools of an
|
|
7
7
|
# MCP instance at RUNTIME (no hand-written manifest) and ingests them as
|
|
8
8
|
# data-tools. Given an McpStore instance + an INJECTABLE MCP client
|
|
9
9
|
# (duck-typed: `#list_tools -> [{name, description, inputSchema}]`), it builds a
|
|
10
|
-
# ToolManifest and REUSES the
|
|
10
|
+
# ToolManifest and REUSES the ingestion path (the:import_tools Command:
|
|
11
11
|
# batch upsert into the ToolStore + hot reload + per-tool report + partial-
|
|
12
12
|
# failure isolation R4). The ToolManifest MCP adapter (`inputSchema`) is reused
|
|
13
13
|
# — no schema parsing here.
|
|
14
14
|
#
|
|
15
|
-
# GENERIC
|
|
15
|
+
# GENERIC: nothing here mentions a consumer/gateway. The MCP instance is DATA in the store.
|
|
16
16
|
#
|
|
17
17
|
# BINDING STRATEGY (this stage's choice, bounded):
|
|
18
18
|
# Each discovered tool becomes an HTTP data-tool that makes a JSON-RPC 2.0
|
|
@@ -23,10 +23,10 @@ module Insika
|
|
|
23
23
|
# runs through the SAME HTTP path as the other data-tools (egress guard, secret
|
|
24
24
|
# headers, hot reload) — no new execution code.
|
|
25
25
|
#
|
|
26
|
-
# Each tool gets `group: "mcp:<instance>"` so the
|
|
26
|
+
# Each tool gets `group: "mcp:<instance>"` so the per-group gating
|
|
27
27
|
# (tools_allow_groups) works for free.
|
|
28
28
|
#
|
|
29
|
-
# DEFERRED / OUT-OF-SCOPE (documented — spec
|
|
29
|
+
# DEFERRED / OUT-OF-SCOPE (documented — spec):
|
|
30
30
|
# - Real MCP transport: only instances with a `url` (http transport) are ingestible;
|
|
31
31
|
# stdio has no HTTP endpoint -> raises a clear error (later work).
|
|
32
32
|
# - MCP session lifecycle (initialize/negotiation/session-id/notifications) and the
|
|
@@ -65,7 +65,7 @@ module Insika
|
|
|
65
65
|
if url.nil?
|
|
66
66
|
raise Insika::ValidationError,
|
|
67
67
|
"MCP instance '#{name}' has no url: live ingestion requires HTTP transport " \
|
|
68
|
-
"(stdio is later work
|
|
68
|
+
"(stdio is later work)"
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
tools = Array((client || @client_factory.call(record)).list_tools)
|
|
@@ -7,11 +7,11 @@ module Insika
|
|
|
7
7
|
# two come apart constantly:
|
|
8
8
|
#
|
|
9
9
|
# · The engine delivers an async subagent's result to the parent as a NEW turn
|
|
10
|
-
#
|
|
10
|
+
# a `user` message the engine wrote.
|
|
11
11
|
# · A guardrail short-circuits with a safe reply — an `assistant` message produced
|
|
12
12
|
# with zero LLM calls.
|
|
13
13
|
# · A consumer composes context blocks into the input it sends (`<memoria> …`,
|
|
14
|
-
# `<
|
|
14
|
+
# `<store_cep_required> …`) — a `user` message the customer never typed.
|
|
15
15
|
# · In an imported transcript, a human operator types after a handoff — an
|
|
16
16
|
# `assistant` message no model produced.
|
|
17
17
|
#
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Insika
|
|
4
|
-
# LLM config v2 resolution
|
|
4
|
+
# LLM config v2 resolution. Turns the three config layers into a
|
|
5
5
|
# single ModelSelection at turn start:
|
|
6
6
|
#
|
|
7
7
|
# Chat (per-session pin) > Agent (profile.model) > Platform default (Settings)
|
|
@@ -14,8 +14,9 @@ module Insika
|
|
|
14
14
|
# model / platform default is NOT pinned -> fallbacks apply.
|
|
15
15
|
# params -> generation params (Hash of symbols: temperature/max_tokens/thinking)
|
|
16
16
|
# fallbacks -> ordered [{ model:, provider: }] to try when NOT pinned. The
|
|
17
|
-
# mid-turn ROTATION across this chain
|
|
18
|
-
#
|
|
17
|
+
# mid-turn ROTATION across this chain (plus the profile's own
|
|
18
|
+
# reliability["fallback"] refs) is WS3's Reliability coordinator;
|
|
19
|
+
# here the chain is resolved + surfaced (source/pinned) for it.
|
|
19
20
|
ModelSelection = Data.define(:model, :provider, :source, :pinned, :params, :fallbacks) do
|
|
20
21
|
def initialize(model:, provider: nil, source: :platform_default, pinned: false,
|
|
21
22
|
params: {}, fallbacks: [])
|
|
@@ -70,7 +71,7 @@ module Insika
|
|
|
70
71
|
chat.with_params(**payload) unless payload.empty?
|
|
71
72
|
end
|
|
72
73
|
|
|
73
|
-
# The resolved reasoning control (
|
|
74
|
+
# The resolved reasoning control (4-layer). Two axes folded into one field:
|
|
74
75
|
# off -> reasoning DISABLED (thinking:{type:disabled})
|
|
75
76
|
# on -> reasoning ENABLED, provider-default effort
|
|
76
77
|
# low|medium|high -> reasoning enabled at that effort (reasoning_effort)
|
|
@@ -105,7 +106,7 @@ module Insika
|
|
|
105
106
|
def present?(v) = Insika::Coercion.present?(v)
|
|
106
107
|
end
|
|
107
108
|
|
|
108
|
-
# The selectable reasoning values (
|
|
109
|
+
# The selectable reasoning values (4-layer). Blank/absent = inherit the
|
|
109
110
|
# broader layer; these are the explicit choices. Shared with the Studio forms.
|
|
110
111
|
# Defined on the class OUTSIDE the Data.define block on purpose: a constant
|
|
111
112
|
# assigned inside the block would land in the enclosing lexical scope (Insika),
|
data/lib/insika/onboarding.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Insika
|
|
4
|
-
# LLM-first onboarding surface
|
|
4
|
+
# LLM-first onboarding surface. The "Flue trick": the insika
|
|
5
5
|
# serves, from itself, a `start.md` addressed to the DEVELOPER'S OWN coding agent
|
|
6
6
|
# ("Read <base>/start.md then help me build my first agent") plus a machine-readable
|
|
7
7
|
# `/models.json` and the public docs mirrored as raw markdown. It is `rails new`
|
|
@@ -31,7 +31,7 @@ module Insika
|
|
|
31
31
|
MODELS_SCHEMA_VERSION = 1
|
|
32
32
|
|
|
33
33
|
# The PUBLIC docs allowlist, repo-relative: slug => path. Explicit on purpose —
|
|
34
|
-
# the gitignored internal docs (
|
|
34
|
+
# the gitignored internal docs (/ TRANSLATION-TRACKER / …)
|
|
35
35
|
# are NEVER here, so /docs can only ever serve OSS material. Kept in sync with the
|
|
36
36
|
# tracked `.md` prose (README + docs/*.md).
|
|
37
37
|
PUBLIC_DOCS = {
|
data/lib/insika/outbox_store.rb
CHANGED
|
@@ -4,11 +4,11 @@ require "securerandom"
|
|
|
4
4
|
require "time"
|
|
5
5
|
|
|
6
6
|
module Insika
|
|
7
|
-
# Durable record of one pending OUTBOUND reply
|
|
7
|
+
# Durable record of one pending OUTBOUND reply. A Shape B
|
|
8
8
|
# channel answers out of band — the turn ends and the reply is POSTed to the
|
|
9
9
|
# platform (or, for a relay, to the consumer's own callback) afterwards — so
|
|
10
10
|
# "the answer exists but the recipient is not on this connection" is exactly
|
|
11
|
-
# the problem
|
|
11
|
+
# the problem already solved for async delegation. This store
|
|
12
12
|
# is DelegationStore's shape with a different recipient, deliberately: a second
|
|
13
13
|
# invention here would be a second thing to get wrong.
|
|
14
14
|
#
|
|
@@ -3,17 +3,16 @@
|
|
|
3
3
|
module Insika
|
|
4
4
|
# DYNAMIC tool registry: composes the CODE registry (base, built at boot,
|
|
5
5
|
# immutable) with the DATA-DEFINED tools from the ToolStore. Drop-in for ToolRegistry —
|
|
6
|
-
# the Executor/ToolCatalog/ToolEnvelope only use entries/resolve/side_effect
|
|
7
|
-
# Step B / D2.
|
|
6
|
+
# the Executor/ToolCatalog/ToolEnvelope only use entries/resolve/side_effect?.,
|
|
8
7
|
#
|
|
9
8
|
# Rules:
|
|
10
9
|
# - COLLISION: the base (code) ALWAYS wins — a data-tool cannot hijack
|
|
11
10
|
# the name of a code tool (security, R3). The authoring Command also
|
|
12
11
|
# refuses to create with a colliding name (code_tool?), but the defense stays here.
|
|
13
12
|
# - HOT: `reload` re-reads the store and swaps the dynamic index atomically — a
|
|
14
|
-
# new/edited data-tool takes effect on the next turn without a restart
|
|
13
|
+
# new/edited data-tool takes effect on the next turn without a restart, mirroring
|
|
15
14
|
# SkillCatalog.reload. An in-flight turn has already captured the index.
|
|
16
|
-
#
|
|
15
|
+
# PARITY: empty ToolStore ⇒ entries/resolve/side_effect? identical to the
|
|
17
16
|
# pure base. The base (config/wiring.rb) does not even use the overlay — zero regression.
|
|
18
17
|
#
|
|
19
18
|
# The data-tools enter as NORMAL Registry::Entry (optional: false) — they obey
|
data/lib/insika/pack.rb
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
5
|
module Insika
|
|
6
|
-
# Provisioning pack
|
|
6
|
+
# Provisioning pack: the PORTABLE form of an agent — a
|
|
7
7
|
# manifest + the prompt files + the skills + the data-tool defs. It's what
|
|
8
8
|
# `docs/prompt-base/06` describes as a workspace, here in a value object
|
|
9
9
|
# consumable by the PackImporter (which emits the authoring Commands). GENERIC
|
|
10
|
-
# per project
|
|
10
|
+
# per project: the engine doesn't know any consumer — the pack is the contract.
|
|
11
11
|
#
|
|
12
12
|
# config: Hash — manifest (AgentProfile.build attrs: id/model/provider/
|
|
13
13
|
# limits/metadata/tools_deferred/…). `id`/`model` required there.
|
|
@@ -16,7 +16,7 @@ module Insika
|
|
|
16
16
|
# skills: { "escalation-to-human" => "<SKILL.md>", ... } — 1 per skill.
|
|
17
17
|
# tools: [ { ToolDefinition hash }, ... ] — the pack's data-tool defs.
|
|
18
18
|
#
|
|
19
|
-
# Two sources: `from_h` (GatewayClient JSON — provisioning API
|
|
19
|
+
# Two sources: `from_h` (GatewayClient JSON — provisioning API) and
|
|
20
20
|
# `from_dir` (a folder on disk per docs/prompt-base/06 — authoring/CLI).
|
|
21
21
|
Pack = Data.define(:config, :files, :skills, :tools) do
|
|
22
22
|
# Raw Hash (string|symbol keys) -> Pack. Tolerates both key conventions
|
data/lib/insika/pack_importer.rb
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Insika
|
|
4
|
-
# Pack importer
|
|
4
|
+
# Pack importer: reads a Pack and emits the ALREADY
|
|
5
5
|
# existing authoring Commands — create_agent/update_agent + write_agent_file +
|
|
6
6
|
# write_skill + write_data_tool — making an agent provisionable at runtime from
|
|
7
|
-
# a standardized pack. It's the piece
|
|
8
|
-
#
|
|
7
|
+
# a standardized pack. It's the piece a consumer's provisioning client
|
|
8
|
+
# triggers (via the provisioning API).
|
|
9
9
|
#
|
|
10
|
-
# GENERIC
|
|
10
|
+
# GENERIC: nothing here mentions a consumer — the pack is the contract. Tool
|
|
11
11
|
# names come from the PACK, so prompts<->tools stay consistent by construction
|
|
12
|
-
#
|
|
12
|
+
# It doesn't write to the store directly: it only dispatches Commands on
|
|
13
13
|
# the bus (the same transport discipline) + READS the ProfileSource to decide
|
|
14
14
|
# create vs update.
|
|
15
15
|
#
|
|
@@ -58,13 +58,15 @@ module Insika
|
|
|
58
58
|
# its OWN pack (per-store isolation) and re-provisioning removes what left.
|
|
59
59
|
# - prompt_files = the pack's .md files (write_agent_file also registers;
|
|
60
60
|
# union is a no-op). Setting here makes the list authoritative (removes the ones that left).
|
|
61
|
-
# - skills = the pack's skills/ dirs (explicit allowlist; []
|
|
62
|
-
#
|
|
63
|
-
# skills from other stores).
|
|
61
|
+
# - skills = the pack's skills/ dirs (explicit allowlist; [] when the pack
|
|
62
|
+
# has none — never nil=all, which would leak skills from other stores).
|
|
64
63
|
# - tools_allow = (config.tools_allow) ∪ (the pack's tool names) — guarantees
|
|
65
|
-
# the agent can call its own data-tools
|
|
64
|
+
# the agent can call its own data-tools. [] when neither exists —
|
|
65
|
+
# never nil=all: the ToolStore is GLOBAL, so a tool-less pack would
|
|
66
|
+
# otherwise see every other store's tools (store A's pack calling
|
|
67
|
+
# store B's set_location is exactly that leak).
|
|
66
68
|
# - tools_allow_groups = groups enabled by FLAG in the pack (see
|
|
67
|
-
# #enabled_groups) — the per-flag schema CUT
|
|
69
|
+
# #enabled_groups) — the per-flag schema CUT: only
|
|
68
70
|
# the tools of the enabled groups (union with tools_allow) go to the model;
|
|
69
71
|
# those of disabled groups are cut BEFORE the turn (resolves the OpenClaw
|
|
70
72
|
# tool-call waste, where the flag only exists in Rails).
|
|
@@ -72,24 +74,24 @@ module Insika
|
|
|
72
74
|
attrs = pack.config.dup
|
|
73
75
|
attrs[:id] = id
|
|
74
76
|
attrs[:prompt_files] = pack.files.keys unless pack.files.empty?
|
|
75
|
-
attrs[:skills] = pack.skills.keys
|
|
77
|
+
attrs[:skills] = pack.skills.keys
|
|
76
78
|
|
|
77
79
|
pack_tools = pack.tools.map { |t| tool_name(t) }
|
|
78
80
|
allow = Array(pack.config[:tools_allow]).map(&:to_s) | pack_tools
|
|
79
|
-
attrs[:tools_allow] = allow
|
|
81
|
+
attrs[:tools_allow] = allow
|
|
80
82
|
|
|
81
83
|
groups = enabled_groups(pack.config)
|
|
82
84
|
attrs[:tools_allow_groups] = groups unless groups.nil?
|
|
83
85
|
attrs
|
|
84
86
|
end
|
|
85
87
|
|
|
86
|
-
# PER-FLAG CUT (
|
|
88
|
+
# PER-FLAG CUT (/, STATIC pilot): derives the agent's
|
|
87
89
|
# per-group allowlist from FLAGS declared in the pack config — DATA, never a
|
|
88
|
-
# core convention
|
|
90
|
+
# core convention. The engine doesn't know "groceries_v2"/"b2b": the pack
|
|
89
91
|
# declares which GROUPS are enabled; the flag->group mapping is the
|
|
90
92
|
# responsibility of provisioning/the pack, not the insika. Two forms (union):
|
|
91
93
|
# - `enabled_groups: ["default", "b2b"]` — explicit list of ON groups.
|
|
92
|
-
# - `flags: { "
|
|
94
|
+
# - `flags: { "wholesale" => true, "outlet" => false }` — the flag key IS the group
|
|
93
95
|
# name; only the truthy ones enter (the false ones CUT the group).
|
|
94
96
|
# Neither declared -> nil (no per-group cut; old behavior).
|
|
95
97
|
# Explicit `[]` (empty enabled_groups, or all flags false) -> no group.
|
|
@@ -64,7 +64,7 @@ module Insika
|
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
# -> [PendingAction] every :pending across all tasks — the approvals inbox
|
|
67
|
-
# (
|
|
67
|
+
# (Studio). Single O(n) scan (vs. open_for per task = O(n·m)); the
|
|
68
68
|
# UI resolves task context afterwards via TaskStore#find.
|
|
69
69
|
def all_open
|
|
70
70
|
@store.list(SCOPE, KEY_PREFIX).filter_map do |key|
|
data/lib/insika/plugin/loader.rb
CHANGED
|
@@ -209,7 +209,7 @@ module Insika
|
|
|
209
209
|
@registries[:workflows].register(name, callable, plugin: @plugin_id, &block)
|
|
210
210
|
end
|
|
211
211
|
|
|
212
|
-
#
|
|
212
|
+
# a channel is an INSTANCE (it holds its credentials and its
|
|
213
213
|
# HTTP client), so unlike a tool there is no factory to defer. Declared-or-
|
|
214
214
|
# ignored like the others: the id is a URL segment, and a plugin quietly
|
|
215
215
|
# mounting a route nobody declared is exactly what the contract list prevents.
|
|
@@ -254,7 +254,7 @@ module Insika
|
|
|
254
254
|
|
|
255
255
|
if kind == :workflow
|
|
256
256
|
warn "[plugin #{@plugin_id}] capability '#{name}' (kind: workflow) registered without a consumer " \
|
|
257
|
-
"in this slice — agent exposure is follow-up
|
|
257
|
+
"in this slice — agent exposure is follow-up"
|
|
258
258
|
end
|
|
259
259
|
|
|
260
260
|
@staged_capabilities << { capability: name, impl_name: impl_name, kind: kind,
|
data/lib/insika/policy/policy.rb
CHANGED
|
@@ -47,7 +47,7 @@ module Insika
|
|
|
47
47
|
# Absorbs ToolRegistry#resolve as a policy: optional without
|
|
48
48
|
# opt-in -> deny; tools_deny -> deny ("deny always wins"); tools_allow
|
|
49
49
|
# with the semantics (nil = all; [] = ∅; [names] = final set).
|
|
50
|
-
#
|
|
50
|
+
# `tools_allow_groups` UNIONS in the groups' tools
|
|
51
51
|
# (the group is GIVEN in the Entry metadata). Both allowlists nil = all.
|
|
52
52
|
class ToolAllowlist < Base
|
|
53
53
|
def decide(request)
|
|
@@ -90,6 +90,7 @@ module Insika
|
|
|
90
90
|
tools_allow: h[:tools_allow], tools_deny: h[:tools_deny] || [],
|
|
91
91
|
tools_allow_groups: h[:tools_allow_groups],
|
|
92
92
|
skills: h[:skills],
|
|
93
|
+
skills_eager: h[:skills_eager],
|
|
93
94
|
context_providers: h[:context_providers],
|
|
94
95
|
workflows_allow: h[:workflows_allow],
|
|
95
96
|
policies: Array(h[:policies]).map(&:to_sym),
|
|
@@ -97,27 +98,32 @@ module Insika
|
|
|
97
98
|
limits: symbolize_limits(h[:limits]),
|
|
98
99
|
approvals_required: h[:approvals_required],
|
|
99
100
|
capabilities: h[:capabilities],
|
|
100
|
-
# subagents
|
|
101
|
+
# subagents: allowlist of child ids; build re-normalizes to
|
|
101
102
|
# [String]. nil round-trips as nil (opt-in: NONE).
|
|
102
103
|
subagents: h[:subagents],
|
|
103
104
|
tools_deferred: h[:tools_deferred],
|
|
104
105
|
memory: h[:memory],
|
|
105
106
|
prompt_caching: h[:prompt_caching],
|
|
106
|
-
|
|
107
|
+
tool_output_compression: h[:tool_output_compression],
|
|
108
|
+
# params/model_policy: the resolver tolerates string keys from
|
|
107
109
|
# the JSON round-trip (ModelResolver#normalize_params / ModelPolicy), so no
|
|
108
110
|
# re-symbolization needed here.
|
|
109
111
|
params: h[:params] || {},
|
|
110
112
|
model_policy: h[:model_policy],
|
|
111
|
-
|
|
113
|
+
budget: h[:budget],
|
|
114
|
+
reliability: h[:reliability],
|
|
115
|
+
alerts: h[:alerts],
|
|
116
|
+
stuck_signal: h[:stuck_signal],
|
|
117
|
+
# guardrails: a plain Hash; Safety::Config tolerates the JSON
|
|
112
118
|
# round-trip (string keys/values), so no re-symbolization here.
|
|
113
119
|
guardrails: h[:guardrails],
|
|
114
|
-
# sandbox
|
|
120
|
+
# sandbox: a plain config Hash; Sandbox.build tolerates the JSON
|
|
115
121
|
# round-trip (string keys), so no re-symbolization here. nil = absent.
|
|
116
122
|
sandbox: h[:sandbox],
|
|
117
|
-
# refinement
|
|
123
|
+
# refinement: a plain config Hash read with string keys by the
|
|
118
124
|
# RunRefinement handler; nil round-trips as nil (= report-only).
|
|
119
125
|
refinement: h[:refinement],
|
|
120
|
-
# capabilities_declared
|
|
126
|
+
# capabilities_declared: flat [String]; build re-normalizes.
|
|
121
127
|
capabilities_declared: h[:capabilities_declared],
|
|
122
128
|
# edge_stream: which internal channels may cross to the customer. {} = neither.
|
|
123
129
|
edge_stream: h[:edge_stream],
|