insika 0.0.1 → 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 +361 -0
- data/LICENSE +21 -0
- data/README.md +136 -2
- data/bin/insika +366 -0
- data/docs/AGENTS.md +618 -0
- data/docs/ARCHITECTURE.md +333 -0
- data/docs/BENCHMARK.md +114 -0
- data/docs/CHANNELS.md +453 -0
- data/docs/CONTEXT.md +117 -0
- data/docs/DEPLOY.md +354 -0
- data/docs/EMBEDDING.md +198 -0
- data/docs/EVALS.md +273 -0
- data/docs/LOADTEST.md +232 -0
- data/docs/OBSERVABILITY.md +374 -0
- data/docs/PLUGINS.md +211 -0
- data/docs/REFINEMENT.md +477 -0
- data/docs/RELEASING.md +70 -0
- data/docs/RUNNING-LOCAL.md +153 -0
- data/docs/SANDBOX.md +114 -0
- data/docs/SECURITY.md +375 -0
- data/docs/SKILLS.md +284 -0
- data/docs/TOOLS.md +302 -0
- data/docs/WHY.md +137 -0
- data/docs/WORKFLOWS.md +225 -0
- data/docs/build.md +14 -0
- data/docs/index.md +68 -0
- data/docs/onboarding/start.md +126 -0
- data/docs/operate.md +12 -0
- data/docs/ship.md +10 -0
- data/docs/understand.md +10 -0
- data/lib/insika/agent_file_store.rb +125 -0
- data/lib/insika/agent_profile.rb +255 -0
- data/lib/insika/alert_dispatcher.rb +139 -0
- data/lib/insika/allowlist.rb +28 -0
- data/lib/insika/baseline_store.rb +74 -0
- data/lib/insika/budget_ledger.rb +135 -0
- data/lib/insika/capability/resolved_tool.rb +34 -0
- data/lib/insika/capability_registry.rb +112 -0
- data/lib/insika/channel_delivery.rb +153 -0
- data/lib/insika/channel_registry.rb +30 -0
- data/lib/insika/channels/relay.rb +178 -0
- data/lib/insika/channels/web/widget.js +283 -0
- data/lib/insika/channels/web.rb +211 -0
- data/lib/insika/channels/webhook.rb +58 -0
- data/lib/insika/chat_builder.rb +303 -0
- data/lib/insika/checkpoint.rb +13 -0
- data/lib/insika/checkpoint_store.rb +153 -0
- data/lib/insika/circuit_state.rb +114 -0
- data/lib/insika/coercion.rb +58 -0
- data/lib/insika/command.rb +32 -0
- data/lib/insika/command_bus.rb +39 -0
- data/lib/insika/commands/agent_payload.rb +43 -0
- data/lib/insika/commands/approve_action.rb +46 -0
- data/lib/insika/commands/cancel_task.rb +33 -0
- data/lib/insika/commands/create_agent.rb +54 -0
- data/lib/insika/commands/create_session.rb +67 -0
- data/lib/insika/commands/delete_agent.rb +33 -0
- data/lib/insika/commands/delete_agent_file.rb +50 -0
- data/lib/insika/commands/delete_data_tool.rb +33 -0
- data/lib/insika/commands/delete_llm_provider.rb +36 -0
- data/lib/insika/commands/delete_mcp.rb +30 -0
- data/lib/insika/commands/delete_skill.rb +43 -0
- data/lib/insika/commands/delete_system_file.rb +29 -0
- data/lib/insika/commands/gate_refinement.rb +245 -0
- data/lib/insika/commands/import_mcp_tools.rb +48 -0
- data/lib/insika/commands/import_tools.rb +81 -0
- data/lib/insika/commands/issue_tenant_token.rb +41 -0
- data/lib/insika/commands/memory_add_note.rb +32 -0
- data/lib/insika/commands/memory_forget_fact.rb +32 -0
- data/lib/insika/commands/memory_put_fact.rb +35 -0
- data/lib/insika/commands/pause_task.rb +29 -0
- data/lib/insika/commands/resolve_refinement.rb +126 -0
- data/lib/insika/commands/restore_agent_file.rb +36 -0
- data/lib/insika/commands/restore_data_tool.rb +34 -0
- data/lib/insika/commands/restore_system_file.rb +31 -0
- data/lib/insika/commands/resume_task.rb +85 -0
- 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 +133 -0
- data/lib/insika/commands/send_message.rb +150 -0
- data/lib/insika/commands/set_agent_tools.rb +39 -0
- data/lib/insika/commands/set_skill_agents.rb +112 -0
- data/lib/insika/commands/trigger_workflow.rb +80 -0
- data/lib/insika/commands/update_agent.rb +49 -0
- data/lib/insika/commands/update_settings.rb +33 -0
- data/lib/insika/commands/upsert_llm_provider.rb +34 -0
- data/lib/insika/commands/upsert_mcp.rb +32 -0
- data/lib/insika/commands/write_agent_file.rb +57 -0
- data/lib/insika/commands/write_data_tool.rb +43 -0
- data/lib/insika/commands/write_golden.rb +58 -0
- data/lib/insika/commands/write_skill.rb +60 -0
- data/lib/insika/commands/write_system_file.rb +31 -0
- data/lib/insika/config_store.rb +89 -0
- data/lib/insika/context/builder.rb +166 -0
- data/lib/insika/context/catalog_provider.rb +23 -0
- data/lib/insika/context/fragment.rb +43 -0
- data/lib/insika/context/priority.rb +30 -0
- data/lib/insika/context/provider.rb +19 -0
- data/lib/insika/context/providers/memory.rb +60 -0
- data/lib/insika/context/providers/prompt.rb +105 -0
- data/lib/insika/context/providers/request.rb +32 -0
- data/lib/insika/context/providers/session.rb +123 -0
- data/lib/insika/context/providers/skill.rb +24 -0
- data/lib/insika/context/providers/skill_trigger.rb +128 -0
- data/lib/insika/context/providers/tool_search.rb +20 -0
- data/lib/insika/context_trace_store.rb +92 -0
- data/lib/insika/delegation_store.rb +153 -0
- data/lib/insika/doctor.rb +539 -0
- data/lib/insika/dsl/definition.rb +55 -0
- data/lib/insika/dsl/runtime.rb +382 -0
- data/lib/insika/dsl/server_boot.rb +98 -0
- data/lib/insika/dsl/system.rb +93 -0
- data/lib/insika/dsl/workflow_adapter.rb +59 -0
- data/lib/insika/dsl.rb +364 -0
- data/lib/insika/edge_limiter.rb +268 -0
- data/lib/insika/egress_guard.rb +75 -0
- data/lib/insika/env_schema.rb +249 -0
- data/lib/insika/errors.rb +201 -0
- data/lib/insika/evals/assertions.rb +247 -0
- data/lib/insika/evals/baseline.rb +69 -0
- data/lib/insika/evals/golden.rb +172 -0
- data/lib/insika/evals/judge.rb +225 -0
- data/lib/insika/evals/pairwise.rb +178 -0
- data/lib/insika/evals/report.rb +115 -0
- data/lib/insika/evals/runner.rb +141 -0
- data/lib/insika/evals/transport.rb +178 -0
- data/lib/insika/event.rb +18 -0
- data/lib/insika/event_stream.rb +132 -0
- data/lib/insika/executor.rb +1995 -0
- data/lib/insika/frontmatter.rb +42 -0
- data/lib/insika/golden_store.rb +145 -0
- data/lib/insika/hooks.rb +48 -0
- data/lib/insika/http_client.rb +63 -0
- data/lib/insika/inbound_log.rb +84 -0
- data/lib/insika/llm_configurator.rb +99 -0
- data/lib/insika/llm_provider_store.rb +83 -0
- data/lib/insika/loop_detector.rb +143 -0
- data/lib/insika/mcp_http_client.rb +67 -0
- data/lib/insika/mcp_store.rb +115 -0
- data/lib/insika/mcp_tool_ingestor.rb +143 -0
- data/lib/insika/memory_store.rb +93 -0
- data/lib/insika/message_origin.rb +76 -0
- data/lib/insika/middleware.rb +36 -0
- data/lib/insika/model_policy.rb +52 -0
- data/lib/insika/model_resolver.rb +176 -0
- data/lib/insika/model_selection.rb +115 -0
- data/lib/insika/onboarding.rb +208 -0
- data/lib/insika/outbox_store.rb +166 -0
- data/lib/insika/overlay_tool_registry.rb +102 -0
- data/lib/insika/pack.rb +102 -0
- data/lib/insika/pack_importer.rb +123 -0
- data/lib/insika/pending_action_store.rb +120 -0
- data/lib/insika/plugin/loader.rb +356 -0
- data/lib/insika/plugin.rb +35 -0
- data/lib/insika/policy/engine.rb +83 -0
- data/lib/insika/policy/policy.rb +120 -0
- data/lib/insika/policy_registry.rb +23 -0
- data/lib/insika/profile_source.rb +143 -0
- data/lib/insika/prompt_catalog.rb +61 -0
- data/lib/insika/provider_error_classifier.rb +160 -0
- data/lib/insika/queue_policy.rb +167 -0
- data/lib/insika/recovery.rb +168 -0
- data/lib/insika/refinement/candidate.rb +159 -0
- data/lib/insika/refinement/evidence_collector.rb +371 -0
- data/lib/insika/refinement/gate.rb +234 -0
- data/lib/insika/refinement/panel.rb +222 -0
- data/lib/insika/refinement/proposer.rb +262 -0
- data/lib/insika/refinement_store.rb +295 -0
- data/lib/insika/registry.rb +59 -0
- data/lib/insika/reliability.rb +185 -0
- data/lib/insika/safety/config.rb +109 -0
- data/lib/insika/safety/detectors.rb +176 -0
- data/lib/insika/safety/factory.rb +102 -0
- data/lib/insika/safety/input_guardrail.rb +102 -0
- data/lib/insika/safety/moderator.rb +94 -0
- data/lib/insika/safety/output_filter.rb +79 -0
- data/lib/insika/safety/output_validator.rb +101 -0
- data/lib/insika/safety/safe_responses.rb +47 -0
- data/lib/insika/sandbox/boundary.rb +93 -0
- data/lib/insika/sandbox/docker.rb +74 -0
- data/lib/insika/sandbox/local.rb +33 -0
- data/lib/insika/sandbox/runner.rb +80 -0
- data/lib/insika/sandbox.rb +85 -0
- data/lib/insika/schema_guard.rb +147 -0
- data/lib/insika/secret_masking.rb +34 -0
- data/lib/insika/server/a2a/agent_card.rb +27 -0
- data/lib/insika/server/a2a/app.rb +112 -0
- data/lib/insika/server/a2a/client.rb +101 -0
- data/lib/insika/server/a2a/errors.rb +32 -0
- data/lib/insika/server/a2a/http.rb +42 -0
- data/lib/insika/server/a2a/message.rb +27 -0
- data/lib/insika/server/a2a/protocol.rb +45 -0
- data/lib/insika/server/a2a/remotes.rb +25 -0
- data/lib/insika/server/a2a/task_projection.rb +40 -0
- data/lib/insika/server/app.rb +1022 -0
- data/lib/insika/server/boot.rb +119 -0
- data/lib/insika/server/rack_app.rb +118 -0
- data/lib/insika/server/responses.rb +165 -0
- data/lib/insika/server/sse_body.rb +96 -0
- data/lib/insika/server/tenant_auth.rb +61 -0
- data/lib/insika/session_actor.rb +162 -0
- data/lib/insika/session_store.rb +143 -0
- data/lib/insika/settings_store.rb +154 -0
- data/lib/insika/shutdown.rb +125 -0
- data/lib/insika/skill_catalog.rb +220 -0
- data/lib/insika/skill_store.rb +127 -0
- data/lib/insika/steer_injector.rb +110 -0
- data/lib/insika/store.rb +52 -0
- data/lib/insika/stores/memory.rb +123 -0
- data/lib/insika/stores/sqlite.rb +183 -0
- data/lib/insika/studio/app.rb +1693 -0
- data/lib/insika/studio/assets/dist/application.css +1 -0
- data/lib/insika/studio/assets/dist/application.js +70 -0
- data/lib/insika/studio/forms.rb +335 -0
- data/lib/insika/studio/nav_icons.rb +31 -0
- data/lib/insika/studio/views/_message.erb +44 -0
- data/lib/insika/studio/views/agent_detail.erb +285 -0
- data/lib/insika/studio/views/agents.erb +63 -0
- data/lib/insika/studio/views/approvals.erb +41 -0
- data/lib/insika/studio/views/chats.erb +34 -0
- data/lib/insika/studio/views/evals.erb +83 -0
- data/lib/insika/studio/views/home.erb +72 -0
- data/lib/insika/studio/views/layout.erb +94 -0
- data/lib/insika/studio/views/login.erb +17 -0
- data/lib/insika/studio/views/mcp.erb +91 -0
- data/lib/insika/studio/views/not_found.erb +5 -0
- data/lib/insika/studio/views/playground.erb +47 -0
- data/lib/insika/studio/views/refinement.erb +234 -0
- data/lib/insika/studio/views/session.erb +137 -0
- data/lib/insika/studio/views/settings.erb +168 -0
- data/lib/insika/studio/views/skills.erb +141 -0
- data/lib/insika/studio/views/system_files.erb +65 -0
- data/lib/insika/studio/views/task.erb +105 -0
- data/lib/insika/studio/views/tasks.erb +33 -0
- data/lib/insika/studio/views/tool_edit.erb +107 -0
- data/lib/insika/studio/views/tools.erb +89 -0
- data/lib/insika/subagent_graph.rb +96 -0
- data/lib/insika/system_file_store.rb +96 -0
- data/lib/insika/task_actor.rb +128 -0
- data/lib/insika/task_store.rb +250 -0
- data/lib/insika/telemetry/pricing.rb +104 -0
- data/lib/insika/telemetry/recorder.rb +228 -0
- data/lib/insika/telemetry.rb +127 -0
- data/lib/insika/testing/store_contract.rb +270 -0
- data/lib/insika/tick.rb +122 -0
- data/lib/insika/token_estimator.rb +16 -0
- data/lib/insika/token_store.rb +168 -0
- data/lib/insika/tool_assembly.rb +140 -0
- data/lib/insika/tool_catalog.rb +89 -0
- data/lib/insika/tool_definition.rb +518 -0
- data/lib/insika/tool_envelope.rb +140 -0
- data/lib/insika/tool_manifest.rb +218 -0
- data/lib/insika/tool_output_compressor.rb +100 -0
- data/lib/insika/tool_registry.rb +21 -0
- data/lib/insika/tool_store.rb +135 -0
- data/lib/insika/tool_trace_store.rb +92 -0
- data/lib/insika/tools/a2a_remote.rb +48 -0
- data/lib/insika/tools/agent_enum.rb +68 -0
- data/lib/insika/tools/concurrency.rb +54 -0
- data/lib/insika/tools/data_defined_tool.rb +219 -0
- data/lib/insika/tools/load_skill.rb +99 -0
- data/lib/insika/tools/remember.rb +53 -0
- data/lib/insika/tools/stuck_signal.rb +44 -0
- data/lib/insika/tools/subagent.rb +75 -0
- data/lib/insika/tools/subagents.rb +77 -0
- data/lib/insika/tools/tool_search.rb +94 -0
- data/lib/insika/turn_output.rb +139 -0
- data/lib/insika/turn_state.rb +162 -0
- data/lib/insika/turn_timing.rb +56 -0
- data/lib/insika/usage_ledger.rb +47 -0
- data/lib/insika/version.rb +3 -1
- data/lib/insika/wiring/graph.rb +249 -0
- data/lib/insika/workflow.rb +185 -0
- data/lib/insika/workflow_registry.rb +33 -0
- data/lib/insika.rb +220 -4
- metadata +412 -8
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
# Routes Commands to handlers registered by the composition root.
|
|
5
|
+
# The bus does NOT distinguish control Commands (synchronous response) from
|
|
6
|
+
# turn Commands (immediate `{task_id:}`) — that is the handler's job; the bus
|
|
7
|
+
# only routes.
|
|
8
|
+
#
|
|
9
|
+
# No lock/mutex: one reactor, cooperative fibers; `dispatch`
|
|
10
|
+
# does no IO of its own.
|
|
11
|
+
class CommandBus
|
|
12
|
+
# Handlers receive their dependencies in their own constructor and emit on
|
|
13
|
+
# their own — the bus only routes.
|
|
14
|
+
def initialize
|
|
15
|
+
@handlers = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# handler: any object responding to #call(command). Re-registering the
|
|
19
|
+
# same type overwrites (last wins — the composition root is the only
|
|
20
|
+
# caller).
|
|
21
|
+
def register(type, handler)
|
|
22
|
+
@handlers[type.to_sym] = handler
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Introspection for the composition roots / graph specs: which command
|
|
26
|
+
# types the bus can route. Read-only; does not expose the handlers.
|
|
27
|
+
def registered?(type) = @handlers.key?(type.to_sym)
|
|
28
|
+
def types = @handlers.keys
|
|
29
|
+
|
|
30
|
+
# -> the handler's result. Unregistered type -> synchronous ValidationError,
|
|
31
|
+
# no Task created (never KeyError/NoMethodError).
|
|
32
|
+
def dispatch(command)
|
|
33
|
+
handler = @handlers[command.type]
|
|
34
|
+
raise Insika::ValidationError, "unknown command: #{command.type}" if handler.nil?
|
|
35
|
+
|
|
36
|
+
handler.call(command)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
module Commands
|
|
5
|
+
# Shared normalization for the agent-authoring payload.
|
|
6
|
+
# The transport delivers string keys (JSON); internal dispatch uses
|
|
7
|
+
# symbols. Here: symbolize the top level, keep only the fields buildable by
|
|
8
|
+
# AgentProfile, and re-symbolize the fields the runtime consumes as symbols
|
|
9
|
+
# (provider, policies, limits keys) — same rule as StoredProfileSource.
|
|
10
|
+
module AgentPayload
|
|
11
|
+
# Fields accepted by AgentProfile.build (order irrelevant).
|
|
12
|
+
FIELDS = %i[id model provider base_prompt prompt_files tools_allow tools_deny
|
|
13
|
+
tools_allow_groups skills skills_eager context_providers workflows_allow policies
|
|
14
|
+
prompt_refs limits approvals_required capabilities subagents tools_deferred
|
|
15
|
+
memory prompt_caching tool_output_compression budget reliability alerts
|
|
16
|
+
stuck_signal
|
|
17
|
+
params model_policy guardrails refinement capabilities_declared
|
|
18
|
+
edge_stream metadata].freeze
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# payload (string|symbol keys) -> Hash of attrs ready for AgentProfile.build.
|
|
23
|
+
def attrs(payload)
|
|
24
|
+
h = symbolize(payload)
|
|
25
|
+
out = {}
|
|
26
|
+
FIELDS.each { |f| out[f] = h[f] if h.key?(f) }
|
|
27
|
+
out[:provider] = presence(out[:provider])&.to_sym if out.key?(:provider)
|
|
28
|
+
out[:policies] = Array(out[:policies]).map(&:to_sym) if out.key?(:policies)
|
|
29
|
+
out[:limits] = out[:limits].transform_keys(&:to_sym) if out[:limits].is_a?(Hash)
|
|
30
|
+
# generation params consumed by symbol key (temperature/max_tokens/thinking).
|
|
31
|
+
out[:params] = out[:params].transform_keys(&:to_sym) if out[:params].is_a?(Hash)
|
|
32
|
+
out
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def symbolize(payload)
|
|
36
|
+
(payload || {}).each_with_object({}) { |(k, v), acc| acc[k.to_sym] = v }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Stable name used by the handlers; the rule lives in Insika::Coercion.
|
|
40
|
+
def presence(str) = Insika::Coercion.presence(str)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Commands
|
|
7
|
+
# Control command: the operator resolves a pending action
|
|
8
|
+
# (human-in-the-loop). Resolves the PendingAction in the store (durable source
|
|
9
|
+
# of truth) and WAKES the turn suspended on :waiting by posting :approval to the
|
|
10
|
+
# live fiber. Order matters: RESOLVE before posting — when the await wakes, the
|
|
11
|
+
# store already has the decision (request_approval re-reads it from there). Crash-safe:
|
|
12
|
+
# with no live fiber the post is a no-op; recovery re-executes and uses the durable decision.
|
|
13
|
+
#
|
|
14
|
+
# Payload `{ pending_id:, decision: "approved"|"rejected", operator?: }`.
|
|
15
|
+
# -> resolved PendingAction.
|
|
16
|
+
class ApproveAction
|
|
17
|
+
def initialize(pending_action_store:, executor:, event_stream:)
|
|
18
|
+
@pending_action_store = pending_action_store
|
|
19
|
+
@executor = executor
|
|
20
|
+
@event_stream = event_stream
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(command)
|
|
24
|
+
p = command.payload
|
|
25
|
+
pending_id = (p[:pending_id] || p["pending_id"]).to_s
|
|
26
|
+
decision = (p[:decision] || p["decision"]).to_s
|
|
27
|
+
raise Insika::ValidationError, "pending_id is required" if pending_id.empty?
|
|
28
|
+
|
|
29
|
+
# operator: from the operator's auth (Control UI) via payload; nil ok.
|
|
30
|
+
operator = p[:operator] || p["operator"] || command.meta[:operator]
|
|
31
|
+
|
|
32
|
+
# resolve() validates the decision and the state (:pending); NotFound if absent.
|
|
33
|
+
resolved = @pending_action_store.resolve(pending_id, decision: decision, operator: operator)
|
|
34
|
+
|
|
35
|
+
@executor.approve(resolved.task_id) # wakes the live fiber (no-op if it crashed)
|
|
36
|
+
@event_stream.emit(Insika::Event.new(
|
|
37
|
+
type: :approval_resolved,
|
|
38
|
+
data: { pending_id: pending_id, decision: resolved.status,
|
|
39
|
+
task_id: resolved.task_id, resolved_by: operator },
|
|
40
|
+
meta: { task_id: resolved.task_id, at: Time.now.utc.iso8601 }
|
|
41
|
+
))
|
|
42
|
+
resolved
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
module Commands
|
|
5
|
+
# Control command: posts `:cancel` to the Task's in-process mailbox
|
|
6
|
+
# and responds synchronously. Payload `{ task_id: String }` -> returns Task.
|
|
7
|
+
#
|
|
8
|
+
# Cancellation is COOPERATIVE: the one that transitions the status is the
|
|
9
|
+
# task's fiber when it drains the mailbox — NEVER this handler. Cancelling a task with no
|
|
10
|
+
# live fiber (terminal or orphaned) is an idempotent no-op.
|
|
11
|
+
class CancelTask
|
|
12
|
+
# executor: object with #cancel(task_id) — the real implementation is
|
|
13
|
+
# `@running[task_id]&.post(:cancel)`; here it's a contract (duck type).
|
|
14
|
+
def initialize(task_store:, executor:)
|
|
15
|
+
@task_store = task_store
|
|
16
|
+
@executor = executor
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call(command)
|
|
20
|
+
task_id = command.payload[:task_id] || command.payload["task_id"]
|
|
21
|
+
raise Insika::ValidationError, "task_id is required" if task_id.to_s.empty?
|
|
22
|
+
|
|
23
|
+
task = @task_store.find(task_id)
|
|
24
|
+
raise Insika::NotFoundError, "task '#{task_id}' not found" unless task
|
|
25
|
+
|
|
26
|
+
@executor.cancel(task_id) # no-op if there is no live fiber in this process
|
|
27
|
+
# Does not transition status nor touch the persisted mailbox_state: it only uses
|
|
28
|
+
# the in-process mailbox (Async::Queue). Returns the current state after the post.
|
|
29
|
+
@task_store.find(task_id)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Commands
|
|
7
|
+
# Control command: creates an agent (AgentProfile) at
|
|
8
|
+
# RUNTIME and persists it in the ProfileSource (ConfigStore). This is the "everyone creates
|
|
9
|
+
# their own BIA". Synchronous; does not create a Task. -> AgentProfile (round-tripped from the store).
|
|
10
|
+
class CreateAgent
|
|
11
|
+
def initialize(profile_source:, event_stream:)
|
|
12
|
+
@profile_source = profile_source
|
|
13
|
+
@event_stream = event_stream
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(command)
|
|
17
|
+
attrs = AgentPayload.attrs(command.payload)
|
|
18
|
+
id = AgentPayload.presence(attrs[:id])
|
|
19
|
+
raise Insika::ValidationError, "id is required" if id.nil?
|
|
20
|
+
# model is OPTIONAL as of v2: omitted -> resolves the platform
|
|
21
|
+
# default_model (Settings) at turn start. An agent with neither its own
|
|
22
|
+
# model nor a platform default fails clearly at the first turn.
|
|
23
|
+
raise Insika::ValidationError, "agent '#{id}' already exists" if @profile_source.fetch(id)
|
|
24
|
+
|
|
25
|
+
profile = Insika::AgentProfile.build(**attrs)
|
|
26
|
+
# definition-time cycle + depth check. Only a profile that
|
|
27
|
+
# introduces edges (non-empty subagents) can create a violation — a
|
|
28
|
+
# childless agent is always a safe leaf. Raises SubagentError (a
|
|
29
|
+
# ValidationError) BEFORE persisting, so a bad graph never lands.
|
|
30
|
+
validate_subagent_graph!(profile)
|
|
31
|
+
@profile_source.put(profile)
|
|
32
|
+
emit(:agent_created, id)
|
|
33
|
+
@profile_source.fetch(id) # returns the persisted profile (symbols already normalized)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# Validates the delegation graph with `profile` added to the current set.
|
|
39
|
+
def validate_subagent_graph!(profile)
|
|
40
|
+
return if Array(profile.subagents).empty?
|
|
41
|
+
|
|
42
|
+
others = @profile_source.all.reject { |p| p.id.to_s == profile.id.to_s }
|
|
43
|
+
Insika::SubagentGraph.validate!(others + [profile])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def emit(type, id)
|
|
47
|
+
@event_stream.emit(Insika::Event.new(
|
|
48
|
+
type: type, data: { agent_id: id },
|
|
49
|
+
meta: { at: Time.now.utc.iso8601 }
|
|
50
|
+
))
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Commands
|
|
7
|
+
# Control command: creates a session and responds synchronously —
|
|
8
|
+
# does not create a Task. Payload `{ vars: {} }` -> returns Session. `id` optional:
|
|
9
|
+
# when present, the session is born with that id (correlation by external key —
|
|
10
|
+
# e.g.: chat.id in the /v1/responses adapter; contextId in A2A). Absent -> uuid.
|
|
11
|
+
class CreateSession
|
|
12
|
+
# event_stream: any object with #emit(event) (tests use a spy).
|
|
13
|
+
def initialize(session_store:, event_stream:)
|
|
14
|
+
@session_store = session_store
|
|
15
|
+
@event_stream = event_stream
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(command)
|
|
19
|
+
# Accepts a symbol key (internal dispatch) and a string key (transport JSON)
|
|
20
|
+
# — normalizes at the handler boundary.
|
|
21
|
+
vars = command.payload[:vars] || command.payload["vars"] || {}
|
|
22
|
+
raise Insika::ValidationError, "vars must be a Hash" unless vars.is_a?(Hash)
|
|
23
|
+
|
|
24
|
+
# Per-chat model pin: `model`/`provider` on the payload become a
|
|
25
|
+
# reserved, collision-safe slot in vars (never rendered in the prompt — the
|
|
26
|
+
# Request provider skips "__"-prefixed vars). The ModelResolver reads it as
|
|
27
|
+
# the highest-precedence layer (Chat > Agent > platform default).
|
|
28
|
+
vars = apply_model_pin(vars.dup, command.payload)
|
|
29
|
+
|
|
30
|
+
id = command.payload[:id] || command.payload["id"]
|
|
31
|
+
session = id ? @session_store.create(id: id, vars: vars) : @session_store.create(vars: vars)
|
|
32
|
+
# :session_created is from the closed catalog (origin: CreateSession handler).
|
|
33
|
+
# meta without task_id/seq (control has no Task); Event#to_h does meta.compact.
|
|
34
|
+
@event_stream.emit(Insika::Event.new(
|
|
35
|
+
type: :session_created,
|
|
36
|
+
data: { session_id: session.id },
|
|
37
|
+
meta: { session_id: session.id, at: Time.now.utc.iso8601 }
|
|
38
|
+
))
|
|
39
|
+
session
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Reads `model`/`provider`/`thinking` (string|symbol keys) and, when any is
|
|
45
|
+
# present, writes the reserved vars slot. All must be strings; `thinking` is
|
|
46
|
+
# the per-chat reasoning override (off/on/low/medium/high) and may be set
|
|
47
|
+
# WITHOUT a model pin. Absent -> vars unchanged.
|
|
48
|
+
def apply_model_pin(vars, payload)
|
|
49
|
+
model = payload[:model] || payload["model"]
|
|
50
|
+
provider = payload[:provider] || payload["provider"]
|
|
51
|
+
thinking = payload[:thinking] || payload["thinking"]
|
|
52
|
+
return vars if model.nil? && provider.nil? && thinking.nil?
|
|
53
|
+
|
|
54
|
+
raise Insika::ValidationError, "model must be a String" unless model.nil? || model.is_a?(String)
|
|
55
|
+
raise Insika::ValidationError, "provider must be a String" unless provider.nil? || provider.is_a?(String)
|
|
56
|
+
raise Insika::ValidationError, "thinking must be a String" unless thinking.nil? || thinking.is_a?(String)
|
|
57
|
+
|
|
58
|
+
pin = {}
|
|
59
|
+
pin["model"] = model if model
|
|
60
|
+
pin["provider"] = provider if provider
|
|
61
|
+
pin["thinking"] = thinking if thinking
|
|
62
|
+
vars[Insika::ModelResolver::SESSION_SLOT] = pin
|
|
63
|
+
vars
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Commands
|
|
7
|
+
# Control command: removes an agent. -> the removed AgentProfile
|
|
8
|
+
# (so the transport can confirm what disappeared). Turns ALREADY in progress
|
|
9
|
+
# that captured the profile keep going until they finish (the ProfileSource only affects
|
|
10
|
+
# new dispatches).
|
|
11
|
+
class DeleteAgent
|
|
12
|
+
def initialize(profile_source:, event_stream:)
|
|
13
|
+
@profile_source = profile_source
|
|
14
|
+
@event_stream = event_stream
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(command)
|
|
18
|
+
id = AgentPayload.presence(AgentPayload.symbolize(command.payload)[:id])
|
|
19
|
+
raise Insika::ValidationError, "id is required" if id.nil?
|
|
20
|
+
|
|
21
|
+
removed = @profile_source.fetch(id) ||
|
|
22
|
+
(raise Insika::NotFoundError, "agent '#{id}' not found")
|
|
23
|
+
|
|
24
|
+
@profile_source.delete(id)
|
|
25
|
+
@event_stream.emit(Insika::Event.new(
|
|
26
|
+
type: :agent_deleted, data: { agent_id: id },
|
|
27
|
+
meta: { at: Time.now.utc.iso8601 }
|
|
28
|
+
))
|
|
29
|
+
removed
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Commands
|
|
7
|
+
# Control command: removes a prompt file from an agent's
|
|
8
|
+
# workspace. Nonexistent -> NotFoundError. -> { agent_id, file }.
|
|
9
|
+
class DeleteAgentFile
|
|
10
|
+
def initialize(profile_source:, agent_file_store:, event_stream:)
|
|
11
|
+
@profile_source = profile_source
|
|
12
|
+
@agent_files = agent_file_store
|
|
13
|
+
@event_stream = event_stream
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(command)
|
|
17
|
+
p = AgentPayload.symbolize(command.payload)
|
|
18
|
+
agent_id = AgentPayload.presence(p[:agent_id])
|
|
19
|
+
file = AgentPayload.presence(p[:file])
|
|
20
|
+
raise Insika::ValidationError, "agent_id is required" if agent_id.nil?
|
|
21
|
+
raise Insika::ValidationError, "file is required" if file.nil?
|
|
22
|
+
|
|
23
|
+
unless @agent_files.delete(agent_id, file)
|
|
24
|
+
raise Insika::NotFoundError, "file '#{file}' not found for agent '#{agent_id}'"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
unregister_prompt_file(agent_id, file)
|
|
28
|
+
@event_stream.emit(Insika::Event.new(
|
|
29
|
+
type: :agent_file_deleted, data: { agent_id: agent_id, file: file },
|
|
30
|
+
meta: { at: Time.now.utc.iso8601 }
|
|
31
|
+
))
|
|
32
|
+
{ agent_id: agent_id, file: file }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Removing the file also drops it from prompt_files (the inverse operation of
|
|
38
|
+
# WriteAgentFile) — the Command's job, not the dispatcher's.
|
|
39
|
+
def unregister_prompt_file(agent_id, file)
|
|
40
|
+
profile = @profile_source.fetch(agent_id)
|
|
41
|
+
return unless profile
|
|
42
|
+
|
|
43
|
+
files = Array(profile.prompt_files).map(&:to_s)
|
|
44
|
+
return unless files.include?(file)
|
|
45
|
+
|
|
46
|
+
@profile_source.put(Insika::AgentProfile.build(**profile.to_h.merge(prompt_files: files - [file])))
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Commands
|
|
7
|
+
# Removes a data tool from the ToolStore and reloads overlay + catalog.
|
|
8
|
+
# 404 if it did not exist. -> { name }.
|
|
9
|
+
class DeleteDataTool
|
|
10
|
+
def initialize(tool_store:, registry:, tool_catalog:, event_stream:)
|
|
11
|
+
@tool_store = tool_store
|
|
12
|
+
@registry = registry
|
|
13
|
+
@tool_catalog = tool_catalog
|
|
14
|
+
@event_stream = event_stream
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(command)
|
|
18
|
+
p = AgentPayload.symbolize(command.payload)
|
|
19
|
+
name = AgentPayload.presence(p[:name])
|
|
20
|
+
raise Insika::ValidationError, "name is required" if name.nil?
|
|
21
|
+
raise Insika::NotFoundError, "tool '#{name}' not found" unless @tool_store.delete(name)
|
|
22
|
+
|
|
23
|
+
@registry.reload
|
|
24
|
+
@tool_catalog.reload
|
|
25
|
+
@event_stream.emit(Insika::Event.new(
|
|
26
|
+
type: :data_tool_deleted, data: { name: name },
|
|
27
|
+
meta: { at: Time.now.utc.iso8601 }
|
|
28
|
+
))
|
|
29
|
+
{ name: name }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Commands
|
|
7
|
+
# Control command: removes an LLM provider from the
|
|
8
|
+
# LLMProviderStore. Idempotent (`existed: false` if there was none). UNDOES the
|
|
9
|
+
# config in RubyLLM at runtime: when the provider existed, calls
|
|
10
|
+
# `configurator.unapply(api)` clearing the global key/base — without a restart. A provider
|
|
11
|
+
# that RubyLLM does not recognize degrades gracefully (unapply: false, nothing to
|
|
12
|
+
# undo). -> { existed: bool }.
|
|
13
|
+
class DeleteLLMProvider
|
|
14
|
+
def initialize(provider_store:, configurator:, event_stream:)
|
|
15
|
+
@provider_store = provider_store
|
|
16
|
+
@configurator = configurator
|
|
17
|
+
@event_stream = event_stream
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(command)
|
|
21
|
+
p = AgentPayload.symbolize(command.payload)
|
|
22
|
+
api = AgentPayload.presence(p[:api])
|
|
23
|
+
raise Insika::ValidationError, "api is required" if api.nil?
|
|
24
|
+
|
|
25
|
+
existed = @provider_store.delete(api)
|
|
26
|
+
@configurator.unapply(api) if existed
|
|
27
|
+
@event_stream.emit(Insika::Event.new(
|
|
28
|
+
type: :llm_provider_deleted,
|
|
29
|
+
data: { api: api, existed: existed },
|
|
30
|
+
meta: { at: Time.now.utc.iso8601 }
|
|
31
|
+
))
|
|
32
|
+
{ existed: existed }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Commands
|
|
7
|
+
# Control command: removes an MCP instance from the
|
|
8
|
+
# McpStore. Idempotent (`existed: false` if there was none). -> { existed: bool }.
|
|
9
|
+
class DeleteMcp
|
|
10
|
+
def initialize(mcp_store:, event_stream:)
|
|
11
|
+
@mcp_store = mcp_store
|
|
12
|
+
@event_stream = event_stream
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(command)
|
|
16
|
+
p = AgentPayload.symbolize(command.payload)
|
|
17
|
+
name = AgentPayload.presence(p[:name])
|
|
18
|
+
raise Insika::ValidationError, "name is required" if name.nil?
|
|
19
|
+
|
|
20
|
+
existed = @mcp_store.delete(name)
|
|
21
|
+
@event_stream.emit(Insika::Event.new(
|
|
22
|
+
type: :mcp_deleted,
|
|
23
|
+
data: { name: name, existed: existed },
|
|
24
|
+
meta: { at: Time.now.utc.iso8601 }
|
|
25
|
+
))
|
|
26
|
+
{ existed: existed }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Commands
|
|
7
|
+
# Control command: removes an authored skill from the SkillStore and RELOADS the
|
|
8
|
+
# catalog. -> { name, agent, deleted }.
|
|
9
|
+
#
|
|
10
|
+
# With `agent`, it deletes only that agent's SPECIALIZATION — the shared skill
|
|
11
|
+
# stays exactly where it was, and the agent falls back to it on the next turn.
|
|
12
|
+
# "Stop specializing this" is the inverse of the specialize action, and it must not
|
|
13
|
+
# be expressible as "delete the skill".
|
|
14
|
+
#
|
|
15
|
+
# Deleting a shared skill does NOT touch any allowlist: an agent left naming a
|
|
16
|
+
# skill that no longer exists sees nothing (the catalog resolves nothing), which is
|
|
17
|
+
# the same outcome as never having allowed it.
|
|
18
|
+
class DeleteSkill
|
|
19
|
+
def initialize(skill_store:, skill_catalog:, event_stream:)
|
|
20
|
+
@skill_store = skill_store
|
|
21
|
+
@skill_catalog = skill_catalog
|
|
22
|
+
@event_stream = event_stream
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call(command)
|
|
26
|
+
p = AgentPayload.symbolize(command.payload)
|
|
27
|
+
name = AgentPayload.presence(p[:name])
|
|
28
|
+
agent = AgentPayload.presence(p[:agent])
|
|
29
|
+
raise Insika::ValidationError, "name is required" if name.nil?
|
|
30
|
+
|
|
31
|
+
deleted = @skill_store.delete(name, agent: agent)
|
|
32
|
+
raise Insika::NotFoundError, "skill '#{name}' not found#{" for agent '#{agent}'" if agent}" unless deleted
|
|
33
|
+
|
|
34
|
+
@skill_catalog.reload
|
|
35
|
+
@event_stream.emit(Insika::Event.new(
|
|
36
|
+
type: :skill_deleted, data: { name: name, agent: agent }.compact,
|
|
37
|
+
meta: { at: Time.now.utc.iso8601 }
|
|
38
|
+
))
|
|
39
|
+
{ name: name, agent: agent, deleted: true }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Commands
|
|
7
|
+
# Control command: removes a global system file
|
|
8
|
+
# from the SystemFileStore. Idempotent (`existed: false`). -> { existed: bool }.
|
|
9
|
+
class DeleteSystemFile
|
|
10
|
+
def initialize(system_file_store:, event_stream:)
|
|
11
|
+
@system_files = system_file_store
|
|
12
|
+
@event_stream = event_stream
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(command)
|
|
16
|
+
p = AgentPayload.symbolize(command.payload)
|
|
17
|
+
file = AgentPayload.presence(p[:file])
|
|
18
|
+
raise Insika::ValidationError, "file is required" if file.nil?
|
|
19
|
+
|
|
20
|
+
existed = @system_files.delete(file)
|
|
21
|
+
@event_stream.emit(Insika::Event.new(
|
|
22
|
+
type: :system_file_deleted, data: { file: file, existed: existed },
|
|
23
|
+
meta: { at: Time.now.utc.iso8601 }
|
|
24
|
+
))
|
|
25
|
+
{ existed: existed }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|