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,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
module Wiring
|
|
5
|
+
# SHARED composition core for both roots: the minimal wiring
|
|
6
|
+
# (config/wiring.rb) and the concrete deployment (config/deployment.rb) were two
|
|
7
|
+
# near-identical composition roots. The duplication — backend→stores, event
|
|
8
|
+
# stream, registries + policy builtins, capability registry, hooks, the Executor,
|
|
9
|
+
# and the core Command Bus — lives here now; each root only layers on what
|
|
10
|
+
# legitimately differs (profiles, plain-vs-overlay tool registry, catalogs,
|
|
11
|
+
# guardrails config, context providers, and the root-specific bus commands).
|
|
12
|
+
#
|
|
13
|
+
# Two phases, no magic (a block would hit dynamic-constant assignment in the
|
|
14
|
+
# roots, which promote everything to public constants):
|
|
15
|
+
# 1. `spine(backend:)` — the parameter-free infra spine.
|
|
16
|
+
# 2. `build(spine:, ...)` — assembles the Executor + Command Bus on top, given
|
|
17
|
+
# the root's divergent collaborators. This is where the 6 CORE commands
|
|
18
|
+
# (incl. pause_task/approve_action) are registered — which is what removes
|
|
19
|
+
# the config.ru / serve_real.rb patch that used to bolt them on afterwards.
|
|
20
|
+
module Graph
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# Backend by config: INSIKA_DB set → durable SQLite (survives restart, the
|
|
24
|
+
# prerequisite for Recovery); missing → ephemeral Memory (dev/demo). The same
|
|
25
|
+
# rule lived verbatim in both roots. Dual-read honors the legacy HARNESS_DB alias.
|
|
26
|
+
def backend_from_env(env = ENV)
|
|
27
|
+
db = Insika::EnvSchema.read("INSIKA_DB", env)
|
|
28
|
+
db && !db.empty? ? Insika::Stores::SQLite.new(path: db) : Insika::Stores::Memory.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# an integer tick knob from the env, nil when unset (the caller's
|
|
32
|
+
# default wins). Dual-read honors the deprecated HARNESS_* alias.
|
|
33
|
+
def tick_env(name, env = ENV)
|
|
34
|
+
value = Insika::EnvSchema.read(name, env)
|
|
35
|
+
value&.to_i
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# the infra spine that is IDENTICAL across roots. `extra_policy_
|
|
39
|
+
# builtins` covers the one real divergence (the minimal wiring also registers
|
|
40
|
+
# :workflow_allowlist; the deployment does not expose workflows).
|
|
41
|
+
def spine(backend:, extra_policy_builtins: {})
|
|
42
|
+
session_store = Insika::SessionStore.new(store: backend)
|
|
43
|
+
task_store = Insika::TaskStore.new(store: backend)
|
|
44
|
+
checkpoint_store = Insika::CheckpointStore.new(store: backend)
|
|
45
|
+
pending_action_store = Insika::PendingActionStore.new(store: backend)
|
|
46
|
+
delegation_store = Insika::DelegationStore.new(store: backend)
|
|
47
|
+
memory_store = Insika::MemoryStore.new(store: backend)
|
|
48
|
+
token_store = Insika::TokenStore.new(store: backend)
|
|
49
|
+
budget_ledger = Insika::BudgetLedger.new(store: backend)
|
|
50
|
+
circuit_state = Insika::CircuitState.new(store: backend)
|
|
51
|
+
# the two durable halves of a Shape B channel — the
|
|
52
|
+
# replies still owed to a platform, and the retry window that stops a
|
|
53
|
+
# redelivered webhook from becoming a second turn. Built unconditionally
|
|
54
|
+
# (they are empty and free when no channel is registered) so a deployment
|
|
55
|
+
# that turns a channel on later finds its state already durable.
|
|
56
|
+
outbox_store = Insika::OutboxStore.new(store: backend)
|
|
57
|
+
inbound_log = Insika::InboundLog.new(store: backend)
|
|
58
|
+
# refinement RUNS (reports over real traffic). Runtime data,
|
|
59
|
+
# same backend as sessions/tasks — the collector and the command that write it
|
|
60
|
+
# are the root's business (deployment-only, like the memory commands).
|
|
61
|
+
refinement_store = Insika::RefinementStore.new(store: backend)
|
|
62
|
+
|
|
63
|
+
code_tool_registry = Insika::ToolRegistry.new
|
|
64
|
+
workflow_registry = Insika::WorkflowRegistry.new
|
|
65
|
+
|
|
66
|
+
policy_registry = Insika::PolicyRegistry.new
|
|
67
|
+
policy_registry.register(:tool_allowlist, Insika::Policy::Builtin::ToolAllowlist)
|
|
68
|
+
policy_registry.register(:skill_allowlist, Insika::Policy::Builtin::SkillAllowlist)
|
|
69
|
+
policy_registry.register(:approval_required, Insika::Policy::Builtin::ApprovalRequired)
|
|
70
|
+
extra_policy_builtins.each { |name, klass| policy_registry.register(name, klass) }
|
|
71
|
+
|
|
72
|
+
Spine.new(
|
|
73
|
+
backend: backend, event_stream: Insika::EventStream.new,
|
|
74
|
+
session_store: session_store, task_store: task_store,
|
|
75
|
+
checkpoint_store: checkpoint_store, pending_action_store: pending_action_store,
|
|
76
|
+
delegation_store: delegation_store,
|
|
77
|
+
memory_store: memory_store, refinement_store: refinement_store,
|
|
78
|
+
token_store: token_store, budget_ledger: budget_ledger, circuit_state: circuit_state,
|
|
79
|
+
outbox_store: outbox_store, inbound_log: inbound_log,
|
|
80
|
+
code_tool_registry: code_tool_registry,
|
|
81
|
+
workflow_registry: workflow_registry, policy_registry: policy_registry,
|
|
82
|
+
capability_registry: Insika::CapabilityRegistry.new, hooks: Insika::Hooks.new,
|
|
83
|
+
channel_registry: Insika::ChannelRegistry.new
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# assemble the graph on top of a spine.
|
|
88
|
+
#
|
|
89
|
+
# tool_registry: effective registry the Executor uses (plain REGISTRY at the
|
|
90
|
+
# base; OverlayToolRegistry in the deployment).
|
|
91
|
+
# guardrails: a Safety::Factory — its input_guardrail becomes the single
|
|
92
|
+
# middleware and its output_validator the after-task hook, so
|
|
93
|
+
# both wirings compose identically.
|
|
94
|
+
# executor_extra: optional Executor kwargs a root adds (deployment passes
|
|
95
|
+
# settings_store + tool_trace_store; the base passes none).
|
|
96
|
+
# edge_limiter: optional EdgeLimiter. It goes BEFORE
|
|
97
|
+
# the InputGuardrail so a flood can't spend the LLM
|
|
98
|
+
# moderator; nil = no edge (parity).
|
|
99
|
+
def build(spine:, profiles:, tool_registry:, tool_catalog:, skill_catalog:,
|
|
100
|
+
prompt_catalog:, guardrails:, context_providers:, edge_limiter: nil,
|
|
101
|
+
executor_extra: {})
|
|
102
|
+
spine.hooks.register(:task, after: guardrails.output_validator)
|
|
103
|
+
middleware = Insika::MiddlewareStack.new([edge_limiter, guardrails.input_guardrail].compact)
|
|
104
|
+
|
|
105
|
+
context_builder = Insika::ContextBuilder.new(
|
|
106
|
+
providers: context_providers, event_stream: spine.event_stream, hooks: spine.hooks
|
|
107
|
+
)
|
|
108
|
+
policy_engine = Insika::Policy::Engine.new(
|
|
109
|
+
policy_registry: spine.policy_registry, event_stream: spine.event_stream
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Always built: the registry starts empty, so `record` finds no
|
|
113
|
+
# channel and returns nil on every turn — the cost of wiring it is one nil
|
|
114
|
+
# check per completed turn, and the alternative is a second wiring path that
|
|
115
|
+
# only production exercises.
|
|
116
|
+
channel_delivery = Insika::ChannelDelivery.new(
|
|
117
|
+
channels: spine.channel_registry, outbox: spine.outbox_store,
|
|
118
|
+
session_store: spine.session_store, event_stream: spine.event_stream
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# WS3 provider reliability (retries/backoff/fallback/breaker) is ALWAYS wired —
|
|
122
|
+
# the profile's `reliability` data gates it, so the bare wiring is unchanged.
|
|
123
|
+
reliability = Insika::Reliability.new(circuit_store: spine.circuit_state,
|
|
124
|
+
event_stream: spine.event_stream)
|
|
125
|
+
executor = Insika::Executor.new(
|
|
126
|
+
context_builder: context_builder, policy_engine: policy_engine,
|
|
127
|
+
middleware: middleware, hooks: spine.hooks,
|
|
128
|
+
tool_registry: tool_registry, skill_catalog: skill_catalog, profiles: profiles,
|
|
129
|
+
session_store: spine.session_store, task_store: spine.task_store,
|
|
130
|
+
checkpoint_store: spine.checkpoint_store, event_stream: spine.event_stream,
|
|
131
|
+
workflow_registry: spine.workflow_registry, pending_action_store: spine.pending_action_store,
|
|
132
|
+
capability_registry: spine.capability_registry, tool_catalog: tool_catalog,
|
|
133
|
+
memory_store: spine.memory_store,
|
|
134
|
+
content_filter_factory: guardrails.content_filter_factory, # stream redaction
|
|
135
|
+
delegation_store: spine.delegation_store, # async delegation durability
|
|
136
|
+
channel_delivery: channel_delivery, # out-of-band reply delivery
|
|
137
|
+
reliability: reliability, # WS3: retries/fallback/breaker (data-gated)
|
|
138
|
+
**executor_extra
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
bus = build_core_bus(spine: spine, profiles: profiles, executor: executor)
|
|
142
|
+
|
|
143
|
+
# the periodic tick (outbox drain + stale recovery sweep). Built
|
|
144
|
+
# here, after the bus, because its recovery half dispatches resume_task
|
|
145
|
+
# through it; handed to the Executor, which starts it as a child of the
|
|
146
|
+
# turn supervisor in serving mode. `INSIKA_TICK_INTERVAL=0` disables.
|
|
147
|
+
executor.tick = Insika::Tick.new(
|
|
148
|
+
store: spine.backend, channel_delivery: channel_delivery,
|
|
149
|
+
recovery: Insika::Recovery.new(
|
|
150
|
+
task_store: spine.task_store, checkpoint_store: spine.checkpoint_store, command_bus: bus
|
|
151
|
+
),
|
|
152
|
+
interval: tick_env("INSIKA_TICK_INTERVAL") || Insika::Tick::DEFAULT_INTERVAL,
|
|
153
|
+
stale_after: tick_env("INSIKA_TICK_STALE_AFTER") || Insika::Tick::DEFAULT_STALE_AFTER
|
|
154
|
+
)
|
|
155
|
+
# WS6 operator alerts: answers budget_warning / breaker_open /
|
|
156
|
+
# delivery_failed per agent (`alerts.webhook`) via the outbox+claim
|
|
157
|
+
# pipeline. Always wired — the per-agent data gates it (parity).
|
|
158
|
+
executor.alert_dispatcher = Insika::AlertDispatcher.new(
|
|
159
|
+
event_stream: spine.event_stream, outbox: spine.outbox_store,
|
|
160
|
+
channels: spine.channel_registry, profiles: profiles,
|
|
161
|
+
task_store: spine.task_store, http: Insika::HttpClient.new
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
Graph::Result.new(
|
|
165
|
+
backend: spine.backend, event_stream: spine.event_stream,
|
|
166
|
+
session_store: spine.session_store, task_store: spine.task_store,
|
|
167
|
+
checkpoint_store: spine.checkpoint_store, pending_action_store: spine.pending_action_store,
|
|
168
|
+
delegation_store: spine.delegation_store,
|
|
169
|
+
memory_store: spine.memory_store, refinement_store: spine.refinement_store,
|
|
170
|
+
outbox_store: spine.outbox_store, inbound_log: spine.inbound_log,
|
|
171
|
+
token_store: spine.token_store, budget_ledger: spine.budget_ledger,
|
|
172
|
+
circuit_state: spine.circuit_state,
|
|
173
|
+
channel_registry: spine.channel_registry, channel_delivery: channel_delivery,
|
|
174
|
+
code_tool_registry: spine.code_tool_registry,
|
|
175
|
+
tool_registry: tool_registry, workflow_registry: spine.workflow_registry,
|
|
176
|
+
policy_registry: spine.policy_registry, capability_registry: spine.capability_registry,
|
|
177
|
+
tool_catalog: tool_catalog, skill_catalog: skill_catalog, prompt_catalog: prompt_catalog,
|
|
178
|
+
hooks: spine.hooks, guardrails: guardrails, middleware: middleware,
|
|
179
|
+
context_providers: context_providers, context_builder: context_builder,
|
|
180
|
+
policy_engine: policy_engine, profiles: profiles, executor: executor, bus: bus
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# The CORE command surface every root needs — turn essentials + the operator
|
|
185
|
+
# controls (pause/approve) the Studio dispatches. Registering pause_task/
|
|
186
|
+
# approve_action HERE is the crux of: it retires the config.ru:28-34 patch.
|
|
187
|
+
def build_core_bus(spine:, profiles:, executor:)
|
|
188
|
+
bus = Insika::CommandBus.new
|
|
189
|
+
bus.register(:create_session,
|
|
190
|
+
Insika::Commands::CreateSession.new(session_store: spine.session_store, event_stream: spine.event_stream))
|
|
191
|
+
bus.register(:cancel_task,
|
|
192
|
+
Insika::Commands::CancelTask.new(task_store: spine.task_store, executor: executor))
|
|
193
|
+
bus.register(:pause_task,
|
|
194
|
+
Insika::Commands::PauseTask.new(task_store: spine.task_store, executor: executor))
|
|
195
|
+
bus.register(:approve_action,
|
|
196
|
+
Insika::Commands::ApproveAction.new(pending_action_store: spine.pending_action_store,
|
|
197
|
+
executor: executor, event_stream: spine.event_stream))
|
|
198
|
+
bus.register(:send_message,
|
|
199
|
+
Insika::Commands::SendMessage.new(profiles: profiles, session_store: spine.session_store,
|
|
200
|
+
task_store: spine.task_store, executor: executor,
|
|
201
|
+
inbound_log: spine.inbound_log))
|
|
202
|
+
bus.register(:resume_task,
|
|
203
|
+
Insika::Commands::ResumeTask.new(profiles: profiles, task_store: spine.task_store,
|
|
204
|
+
checkpoint_store: spine.checkpoint_store, executor: executor))
|
|
205
|
+
# WS1 multi-tenant credentials: per-tenant + operator tokens, stored as
|
|
206
|
+
# hashes. Operator-only BY CONSTRUCTION — the edge refuses a tenant
|
|
207
|
+
# principal on POST /v1/commands, and the handlers re-check meta.
|
|
208
|
+
bus.register(:issue_tenant_token,
|
|
209
|
+
Insika::Commands::IssueTenantToken.new(token_store: spine.token_store,
|
|
210
|
+
event_stream: spine.event_stream))
|
|
211
|
+
bus.register(:revoke_token,
|
|
212
|
+
Insika::Commands::RevokeToken.new(token_store: spine.token_store,
|
|
213
|
+
event_stream: spine.event_stream))
|
|
214
|
+
bus.register(:rotate_tenant_token,
|
|
215
|
+
Insika::Commands::RotateTenantToken.new(token_store: spine.token_store,
|
|
216
|
+
event_stream: spine.event_stream))
|
|
217
|
+
bus
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Infra spine (phase 1 output). Value object — the roots read these to promote
|
|
221
|
+
# them to their historic public constants (SESSION_STORE, REGISTRY, ...).
|
|
222
|
+
Spine = Struct.new(
|
|
223
|
+
:backend, :event_stream, :session_store, :task_store, :checkpoint_store,
|
|
224
|
+
:pending_action_store, :delegation_store, :memory_store, :refinement_store,
|
|
225
|
+
:token_store, :budget_ledger, :circuit_state, :outbox_store, :inbound_log, :code_tool_registry,
|
|
226
|
+
:workflow_registry, :policy_registry, :capability_registry, :hooks,
|
|
227
|
+
:channel_registry, keyword_init: true
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Full graph (phase 2 output). `code_tool_registry` is the plain code registry
|
|
231
|
+
# (REGISTRY); `tool_registry` is the effective one the Executor uses (the
|
|
232
|
+
# deployment's overlay, or the same code registry at the base).
|
|
233
|
+
Result = Struct.new(
|
|
234
|
+
:backend, :event_stream,
|
|
235
|
+
:session_store, :task_store, :checkpoint_store, :pending_action_store, :delegation_store,
|
|
236
|
+
:memory_store, :refinement_store, :outbox_store, :inbound_log, :token_store,
|
|
237
|
+
:budget_ledger, :circuit_state, :channel_registry, :channel_delivery,
|
|
238
|
+
:code_tool_registry, :tool_registry, :workflow_registry, :policy_registry, :capability_registry,
|
|
239
|
+
:tool_catalog, :skill_catalog, :prompt_catalog,
|
|
240
|
+
:hooks, :guardrails, :middleware,
|
|
241
|
+
:context_providers, :context_builder, :policy_engine,
|
|
242
|
+
:profiles, :executor, :bus,
|
|
243
|
+
keyword_init: true
|
|
244
|
+
) do
|
|
245
|
+
def durable? = backend.is_a?(Insika::Stores::SQLite)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
# Workflow surface (COMPETITIVE-ANALYSIS). A workflow is a Ruby
|
|
5
|
+
# callable orchestrating RubyLLM Agents/Workflows FROM WITHIN (RubyLLM First);
|
|
6
|
+
# its durable RUN is a Task (checkpointed, recoverable — stronger than Flue's
|
|
7
|
+
# at-most-once run record). This module adds the EXPOSED surface the consumer
|
|
8
|
+
# asked for, matching the honest Flue scope (not Temporal):
|
|
9
|
+
# · runId — the run id (== task id; the Task IS the run record).
|
|
10
|
+
# · event stream — :workflow_started / :workflow_completed on the task stream.
|
|
11
|
+
# · I/O by schema — optional input_schema / output_schema validated at the edges.
|
|
12
|
+
#
|
|
13
|
+
# A schema is any dry-schema-compatible validator: `#call(value)` returning a
|
|
14
|
+
# result that responds to `#success?` and `#errors` (dry-schema's own contract).
|
|
15
|
+
# As a batteries-included default, a plain JSON Schema Hash (the interlingua the
|
|
16
|
+
# ToolDefinition already speaks) is accepted and validated by the zero-dependency
|
|
17
|
+
# Schema below — "config over convention": bring dry-schema for richer contracts,
|
|
18
|
+
# or hand a JSON Schema for the common case.
|
|
19
|
+
module Workflow
|
|
20
|
+
# Bundles a registered workflow's callable factory with its metadata (schemas +
|
|
21
|
+
# description). Built lazily by WorkflowRegistry#definition — it does NOT resolve
|
|
22
|
+
# the factory (the Executor resolves inside the fiber; the contract forbids
|
|
23
|
+
# instantiating outside it). Schema validation touches only the schemas.
|
|
24
|
+
Definition = Data.define(:name, :description, :input_schema, :output_schema, :factory) do
|
|
25
|
+
# Raises WorkflowSchemaError (< ValidationError) when the input does not
|
|
26
|
+
# conform. No-op without an input_schema (parity). Called synchronously by
|
|
27
|
+
# TriggerWorkflow -> a bad input is a 422 with no run created.
|
|
28
|
+
def validate_input!(input) = enforce!(input_schema, input, :input)
|
|
29
|
+
|
|
30
|
+
# Raises WorkflowSchemaError when the RETURN does not conform. No-op without an
|
|
31
|
+
# output_schema. Called inside the fiber after the workflow returns -> a bad
|
|
32
|
+
# output fails the run at the :workflow_schema stage.
|
|
33
|
+
def validate_output!(output) = enforce!(output_schema, output, :output)
|
|
34
|
+
|
|
35
|
+
# Resolves the factory (INSIDE the fiber) and invokes the orchestrator with the
|
|
36
|
+
# canonical signature `#call(input, context:, tools:)`.
|
|
37
|
+
def call(input, context:, tools:)
|
|
38
|
+
factory.call.call(input, context: context, tools: tools)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Discovery view (GET /v1/workflows): name + description + the I/O contract.
|
|
42
|
+
# A JSON-Schema-backed schema exposes its schema Hash; a duck-typed validator
|
|
43
|
+
# (dry-schema etc.) is opaque to introspection -> "opaque".
|
|
44
|
+
def catalog_entry
|
|
45
|
+
{
|
|
46
|
+
"name" => name,
|
|
47
|
+
"description" => description,
|
|
48
|
+
"input_schema" => schema_view(input_schema),
|
|
49
|
+
"output_schema" => schema_view(output_schema)
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def enforce!(schema, value, phase)
|
|
56
|
+
return if schema.nil?
|
|
57
|
+
|
|
58
|
+
result = schema.call(value)
|
|
59
|
+
return if result.success?
|
|
60
|
+
|
|
61
|
+
raise Insika::WorkflowSchemaError.new(
|
|
62
|
+
"workflow '#{name}' #{phase}", phase: phase, errors: normalize_errors(result.errors)
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# dry-schema returns a MessageSet (responds to #to_h); the built-in Schema
|
|
67
|
+
# already returns a Hash.
|
|
68
|
+
def normalize_errors(errors) = errors.respond_to?(:to_h) ? errors.to_h : errors
|
|
69
|
+
|
|
70
|
+
def schema_view(schema)
|
|
71
|
+
return nil if schema.nil?
|
|
72
|
+
|
|
73
|
+
schema.respond_to?(:json_schema) ? schema.json_schema : "opaque"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Zero-dependency instance validator for a plain JSON Schema Hash, speaking the
|
|
78
|
+
# same safe subset (object/array/string/number/integer/boolean + enum) the
|
|
79
|
+
# ToolDefinition validates its parameters against. It implements the SAME
|
|
80
|
+
# `#call(value) -> result{#success?, #errors}` contract as dry-schema, so the
|
|
81
|
+
# Definition treats both uniformly. Not a full JSON Schema engine (no
|
|
82
|
+
# composition/$ref — the subset the insika supports everywhere); permissive on
|
|
83
|
+
# unknown keys (JSON Schema's additionalProperties default).
|
|
84
|
+
class Schema
|
|
85
|
+
# dry-schema-compatible result. `errors` is { "field.path" => ["message", …] }.
|
|
86
|
+
Result = Data.define(:errors) do
|
|
87
|
+
def success? = errors.empty?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# nil -> nil; a callable (dry-schema / proc) -> as-is; a JSON Schema Hash ->
|
|
91
|
+
# wrapped. Idempotent for an existing Schema (it is callable).
|
|
92
|
+
def self.coerce(schema)
|
|
93
|
+
return nil if schema.nil?
|
|
94
|
+
return schema if schema.respond_to?(:call)
|
|
95
|
+
|
|
96
|
+
new(schema)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
attr_reader :json_schema
|
|
100
|
+
|
|
101
|
+
def initialize(json_schema)
|
|
102
|
+
unless json_schema.is_a?(Hash)
|
|
103
|
+
raise Insika::ValidationError, "workflow schema must be a JSON Schema object or a #call-able validator"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
@json_schema = Insika::Coercion.deep_stringify(json_schema)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def call(value)
|
|
110
|
+
Result.new(errors: collect(@json_schema, value, "").freeze)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
# -> { path => [messages] }. Empty = valid.
|
|
116
|
+
def collect(schema, value, path)
|
|
117
|
+
type = schema["type"].to_s
|
|
118
|
+
return { key(path) => ["must be #{type}, got #{ruby_type(value)}"] } unless type.empty? || matches?(type, value)
|
|
119
|
+
|
|
120
|
+
errors = {}
|
|
121
|
+
case type
|
|
122
|
+
when "object" then check_object(schema, value, path, errors)
|
|
123
|
+
when "array" then check_array(schema, value, path, errors)
|
|
124
|
+
end
|
|
125
|
+
check_enum(schema, value, path, errors)
|
|
126
|
+
errors
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def check_object(schema, value, path, errors)
|
|
130
|
+
props = schema["properties"] || {}
|
|
131
|
+
Array(schema["required"]).each do |name|
|
|
132
|
+
errors[key(join(path, name))] = ["is required"] unless value.key?(name.to_s) || value.key?(name.to_sym)
|
|
133
|
+
end
|
|
134
|
+
props.each do |name, subschema|
|
|
135
|
+
next unless value.key?(name.to_s) || value.key?(name.to_sym)
|
|
136
|
+
|
|
137
|
+
sub = value[name.to_s] || value[name.to_sym]
|
|
138
|
+
errors.merge!(collect(subschema || {}, sub, join(path, name)))
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def check_array(schema, value, path, errors)
|
|
143
|
+
items = schema["items"]
|
|
144
|
+
return if items.nil?
|
|
145
|
+
|
|
146
|
+
value.each_with_index { |el, i| errors.merge!(collect(items, el, "#{join(path, i)}")) }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def check_enum(schema, value, path, errors)
|
|
150
|
+
return unless schema.key?("enum")
|
|
151
|
+
|
|
152
|
+
allowed = Array(schema["enum"])
|
|
153
|
+
errors[key(path)] = ["must be one of #{allowed.inspect}"] unless allowed.include?(value)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def matches?(type, value)
|
|
157
|
+
case type
|
|
158
|
+
when "object" then value.is_a?(Hash)
|
|
159
|
+
when "array" then value.is_a?(Array)
|
|
160
|
+
when "string" then value.is_a?(String)
|
|
161
|
+
when "integer" then value.is_a?(Integer) && !value.is_a?(TrueClass) && !value.is_a?(FalseClass)
|
|
162
|
+
when "number" then value.is_a?(Numeric) && !value.is_a?(TrueClass) && !value.is_a?(FalseClass)
|
|
163
|
+
when "boolean" then value == true || value == false
|
|
164
|
+
else true # unknown type in the schema -> do not block (permissive)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def ruby_type(value)
|
|
169
|
+
case value
|
|
170
|
+
when Hash then "object"
|
|
171
|
+
when Array then "array"
|
|
172
|
+
when String then "string"
|
|
173
|
+
when Integer then "integer"
|
|
174
|
+
when Numeric then "number"
|
|
175
|
+
when true, false then "boolean"
|
|
176
|
+
when nil then "null"
|
|
177
|
+
else value.class.name.downcase
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def join(path, segment) = path.empty? ? segment.to_s : "#{path}.#{segment}"
|
|
182
|
+
def key(path) = path.empty? ? "(root)" : path
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
# Workflow registry. A workflow is a Ruby callable with the signature
|
|
5
|
+
# `#call(input, context:, tools:)` that orchestrates RubyLLM Agents/Workflows FROM
|
|
6
|
+
# WITHIN (RubyLLM First): `context:` is the ContextPackage, `tools:` are
|
|
7
|
+
# instances already filtered by the Resolution. Execution = one logical turn;
|
|
8
|
+
# checkpoint at the end. Nothing is validated at registration (the callable may come
|
|
9
|
+
# via a factory block). Consumed by the TriggerWorkflow handler.
|
|
10
|
+
#
|
|
11
|
+
# — the EXPOSED surface: a registration may carry `input_schema:` /
|
|
12
|
+
# `output_schema:` (a JSON Schema Hash or any dry-schema-compatible validator) and a
|
|
13
|
+
# `description:` in its metadata. `definition` bundles them with the factory into a
|
|
14
|
+
# Workflow::Definition (schemas resolved, factory left lazy); `catalog` is the
|
|
15
|
+
# discovery view (GET /v1/workflows).
|
|
16
|
+
class WorkflowRegistry < Registry
|
|
17
|
+
# -> Workflow::Definition (does NOT resolve the factory — the Executor resolves
|
|
18
|
+
# inside the fiber). Raises NotFoundError for an unregistered name.
|
|
19
|
+
def definition(name)
|
|
20
|
+
e = entry(name)
|
|
21
|
+
Insika::Workflow::Definition.new(
|
|
22
|
+
name: e.name,
|
|
23
|
+
description: e.metadata[:description],
|
|
24
|
+
input_schema: Insika::Workflow::Schema.coerce(e.metadata[:input_schema]),
|
|
25
|
+
output_schema: Insika::Workflow::Schema.coerce(e.metadata[:output_schema]),
|
|
26
|
+
factory: e.factory
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Discovery: [{ "name", "description", "input_schema", "output_schema" }, …].
|
|
31
|
+
def catalog = entries.map { |e| definition(e.name).catalog_entry }
|
|
32
|
+
end
|
|
33
|
+
end
|