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.
Files changed (277) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +361 -0
  3. data/LICENSE +21 -0
  4. data/README.md +136 -2
  5. data/bin/insika +366 -0
  6. data/docs/AGENTS.md +618 -0
  7. data/docs/ARCHITECTURE.md +333 -0
  8. data/docs/BENCHMARK.md +114 -0
  9. data/docs/CHANNELS.md +453 -0
  10. data/docs/CONTEXT.md +117 -0
  11. data/docs/DEPLOY.md +354 -0
  12. data/docs/EMBEDDING.md +198 -0
  13. data/docs/EVALS.md +273 -0
  14. data/docs/LOADTEST.md +232 -0
  15. data/docs/OBSERVABILITY.md +374 -0
  16. data/docs/PLUGINS.md +211 -0
  17. data/docs/REFINEMENT.md +477 -0
  18. data/docs/RELEASING.md +70 -0
  19. data/docs/RUNNING-LOCAL.md +153 -0
  20. data/docs/SANDBOX.md +114 -0
  21. data/docs/SECURITY.md +375 -0
  22. data/docs/SKILLS.md +284 -0
  23. data/docs/TOOLS.md +302 -0
  24. data/docs/WHY.md +137 -0
  25. data/docs/WORKFLOWS.md +225 -0
  26. data/docs/build.md +14 -0
  27. data/docs/index.md +68 -0
  28. data/docs/onboarding/start.md +126 -0
  29. data/docs/operate.md +12 -0
  30. data/docs/ship.md +10 -0
  31. data/docs/understand.md +10 -0
  32. data/lib/insika/agent_file_store.rb +125 -0
  33. data/lib/insika/agent_profile.rb +255 -0
  34. data/lib/insika/alert_dispatcher.rb +139 -0
  35. data/lib/insika/allowlist.rb +28 -0
  36. data/lib/insika/baseline_store.rb +74 -0
  37. data/lib/insika/budget_ledger.rb +135 -0
  38. data/lib/insika/capability/resolved_tool.rb +34 -0
  39. data/lib/insika/capability_registry.rb +112 -0
  40. data/lib/insika/channel_delivery.rb +153 -0
  41. data/lib/insika/channel_registry.rb +30 -0
  42. data/lib/insika/channels/relay.rb +178 -0
  43. data/lib/insika/channels/web/widget.js +283 -0
  44. data/lib/insika/channels/web.rb +211 -0
  45. data/lib/insika/channels/webhook.rb +58 -0
  46. data/lib/insika/chat_builder.rb +303 -0
  47. data/lib/insika/checkpoint.rb +13 -0
  48. data/lib/insika/checkpoint_store.rb +153 -0
  49. data/lib/insika/circuit_state.rb +114 -0
  50. data/lib/insika/coercion.rb +58 -0
  51. data/lib/insika/command.rb +32 -0
  52. data/lib/insika/command_bus.rb +39 -0
  53. data/lib/insika/commands/agent_payload.rb +43 -0
  54. data/lib/insika/commands/approve_action.rb +46 -0
  55. data/lib/insika/commands/cancel_task.rb +33 -0
  56. data/lib/insika/commands/create_agent.rb +54 -0
  57. data/lib/insika/commands/create_session.rb +67 -0
  58. data/lib/insika/commands/delete_agent.rb +33 -0
  59. data/lib/insika/commands/delete_agent_file.rb +50 -0
  60. data/lib/insika/commands/delete_data_tool.rb +33 -0
  61. data/lib/insika/commands/delete_llm_provider.rb +36 -0
  62. data/lib/insika/commands/delete_mcp.rb +30 -0
  63. data/lib/insika/commands/delete_skill.rb +43 -0
  64. data/lib/insika/commands/delete_system_file.rb +29 -0
  65. data/lib/insika/commands/gate_refinement.rb +245 -0
  66. data/lib/insika/commands/import_mcp_tools.rb +48 -0
  67. data/lib/insika/commands/import_tools.rb +81 -0
  68. data/lib/insika/commands/issue_tenant_token.rb +41 -0
  69. data/lib/insika/commands/memory_add_note.rb +32 -0
  70. data/lib/insika/commands/memory_forget_fact.rb +32 -0
  71. data/lib/insika/commands/memory_put_fact.rb +35 -0
  72. data/lib/insika/commands/pause_task.rb +29 -0
  73. data/lib/insika/commands/resolve_refinement.rb +126 -0
  74. data/lib/insika/commands/restore_agent_file.rb +36 -0
  75. data/lib/insika/commands/restore_data_tool.rb +34 -0
  76. data/lib/insika/commands/restore_system_file.rb +31 -0
  77. data/lib/insika/commands/resume_task.rb +85 -0
  78. data/lib/insika/commands/revoke_token.rb +39 -0
  79. data/lib/insika/commands/rotate_tenant_token.rb +43 -0
  80. data/lib/insika/commands/run_refinement.rb +133 -0
  81. data/lib/insika/commands/send_message.rb +150 -0
  82. data/lib/insika/commands/set_agent_tools.rb +39 -0
  83. data/lib/insika/commands/set_skill_agents.rb +112 -0
  84. data/lib/insika/commands/trigger_workflow.rb +80 -0
  85. data/lib/insika/commands/update_agent.rb +49 -0
  86. data/lib/insika/commands/update_settings.rb +33 -0
  87. data/lib/insika/commands/upsert_llm_provider.rb +34 -0
  88. data/lib/insika/commands/upsert_mcp.rb +32 -0
  89. data/lib/insika/commands/write_agent_file.rb +57 -0
  90. data/lib/insika/commands/write_data_tool.rb +43 -0
  91. data/lib/insika/commands/write_golden.rb +58 -0
  92. data/lib/insika/commands/write_skill.rb +60 -0
  93. data/lib/insika/commands/write_system_file.rb +31 -0
  94. data/lib/insika/config_store.rb +89 -0
  95. data/lib/insika/context/builder.rb +166 -0
  96. data/lib/insika/context/catalog_provider.rb +23 -0
  97. data/lib/insika/context/fragment.rb +43 -0
  98. data/lib/insika/context/priority.rb +30 -0
  99. data/lib/insika/context/provider.rb +19 -0
  100. data/lib/insika/context/providers/memory.rb +60 -0
  101. data/lib/insika/context/providers/prompt.rb +105 -0
  102. data/lib/insika/context/providers/request.rb +32 -0
  103. data/lib/insika/context/providers/session.rb +123 -0
  104. data/lib/insika/context/providers/skill.rb +24 -0
  105. data/lib/insika/context/providers/skill_trigger.rb +128 -0
  106. data/lib/insika/context/providers/tool_search.rb +20 -0
  107. data/lib/insika/context_trace_store.rb +92 -0
  108. data/lib/insika/delegation_store.rb +153 -0
  109. data/lib/insika/doctor.rb +539 -0
  110. data/lib/insika/dsl/definition.rb +55 -0
  111. data/lib/insika/dsl/runtime.rb +382 -0
  112. data/lib/insika/dsl/server_boot.rb +98 -0
  113. data/lib/insika/dsl/system.rb +93 -0
  114. data/lib/insika/dsl/workflow_adapter.rb +59 -0
  115. data/lib/insika/dsl.rb +364 -0
  116. data/lib/insika/edge_limiter.rb +268 -0
  117. data/lib/insika/egress_guard.rb +75 -0
  118. data/lib/insika/env_schema.rb +249 -0
  119. data/lib/insika/errors.rb +201 -0
  120. data/lib/insika/evals/assertions.rb +247 -0
  121. data/lib/insika/evals/baseline.rb +69 -0
  122. data/lib/insika/evals/golden.rb +172 -0
  123. data/lib/insika/evals/judge.rb +225 -0
  124. data/lib/insika/evals/pairwise.rb +178 -0
  125. data/lib/insika/evals/report.rb +115 -0
  126. data/lib/insika/evals/runner.rb +141 -0
  127. data/lib/insika/evals/transport.rb +178 -0
  128. data/lib/insika/event.rb +18 -0
  129. data/lib/insika/event_stream.rb +132 -0
  130. data/lib/insika/executor.rb +1995 -0
  131. data/lib/insika/frontmatter.rb +42 -0
  132. data/lib/insika/golden_store.rb +145 -0
  133. data/lib/insika/hooks.rb +48 -0
  134. data/lib/insika/http_client.rb +63 -0
  135. data/lib/insika/inbound_log.rb +84 -0
  136. data/lib/insika/llm_configurator.rb +99 -0
  137. data/lib/insika/llm_provider_store.rb +83 -0
  138. data/lib/insika/loop_detector.rb +143 -0
  139. data/lib/insika/mcp_http_client.rb +67 -0
  140. data/lib/insika/mcp_store.rb +115 -0
  141. data/lib/insika/mcp_tool_ingestor.rb +143 -0
  142. data/lib/insika/memory_store.rb +93 -0
  143. data/lib/insika/message_origin.rb +76 -0
  144. data/lib/insika/middleware.rb +36 -0
  145. data/lib/insika/model_policy.rb +52 -0
  146. data/lib/insika/model_resolver.rb +176 -0
  147. data/lib/insika/model_selection.rb +115 -0
  148. data/lib/insika/onboarding.rb +208 -0
  149. data/lib/insika/outbox_store.rb +166 -0
  150. data/lib/insika/overlay_tool_registry.rb +102 -0
  151. data/lib/insika/pack.rb +102 -0
  152. data/lib/insika/pack_importer.rb +123 -0
  153. data/lib/insika/pending_action_store.rb +120 -0
  154. data/lib/insika/plugin/loader.rb +356 -0
  155. data/lib/insika/plugin.rb +35 -0
  156. data/lib/insika/policy/engine.rb +83 -0
  157. data/lib/insika/policy/policy.rb +120 -0
  158. data/lib/insika/policy_registry.rb +23 -0
  159. data/lib/insika/profile_source.rb +143 -0
  160. data/lib/insika/prompt_catalog.rb +61 -0
  161. data/lib/insika/provider_error_classifier.rb +160 -0
  162. data/lib/insika/queue_policy.rb +167 -0
  163. data/lib/insika/recovery.rb +168 -0
  164. data/lib/insika/refinement/candidate.rb +159 -0
  165. data/lib/insika/refinement/evidence_collector.rb +371 -0
  166. data/lib/insika/refinement/gate.rb +234 -0
  167. data/lib/insika/refinement/panel.rb +222 -0
  168. data/lib/insika/refinement/proposer.rb +262 -0
  169. data/lib/insika/refinement_store.rb +295 -0
  170. data/lib/insika/registry.rb +59 -0
  171. data/lib/insika/reliability.rb +185 -0
  172. data/lib/insika/safety/config.rb +109 -0
  173. data/lib/insika/safety/detectors.rb +176 -0
  174. data/lib/insika/safety/factory.rb +102 -0
  175. data/lib/insika/safety/input_guardrail.rb +102 -0
  176. data/lib/insika/safety/moderator.rb +94 -0
  177. data/lib/insika/safety/output_filter.rb +79 -0
  178. data/lib/insika/safety/output_validator.rb +101 -0
  179. data/lib/insika/safety/safe_responses.rb +47 -0
  180. data/lib/insika/sandbox/boundary.rb +93 -0
  181. data/lib/insika/sandbox/docker.rb +74 -0
  182. data/lib/insika/sandbox/local.rb +33 -0
  183. data/lib/insika/sandbox/runner.rb +80 -0
  184. data/lib/insika/sandbox.rb +85 -0
  185. data/lib/insika/schema_guard.rb +147 -0
  186. data/lib/insika/secret_masking.rb +34 -0
  187. data/lib/insika/server/a2a/agent_card.rb +27 -0
  188. data/lib/insika/server/a2a/app.rb +112 -0
  189. data/lib/insika/server/a2a/client.rb +101 -0
  190. data/lib/insika/server/a2a/errors.rb +32 -0
  191. data/lib/insika/server/a2a/http.rb +42 -0
  192. data/lib/insika/server/a2a/message.rb +27 -0
  193. data/lib/insika/server/a2a/protocol.rb +45 -0
  194. data/lib/insika/server/a2a/remotes.rb +25 -0
  195. data/lib/insika/server/a2a/task_projection.rb +40 -0
  196. data/lib/insika/server/app.rb +1022 -0
  197. data/lib/insika/server/boot.rb +119 -0
  198. data/lib/insika/server/rack_app.rb +118 -0
  199. data/lib/insika/server/responses.rb +165 -0
  200. data/lib/insika/server/sse_body.rb +96 -0
  201. data/lib/insika/server/tenant_auth.rb +61 -0
  202. data/lib/insika/session_actor.rb +162 -0
  203. data/lib/insika/session_store.rb +143 -0
  204. data/lib/insika/settings_store.rb +154 -0
  205. data/lib/insika/shutdown.rb +125 -0
  206. data/lib/insika/skill_catalog.rb +220 -0
  207. data/lib/insika/skill_store.rb +127 -0
  208. data/lib/insika/steer_injector.rb +110 -0
  209. data/lib/insika/store.rb +52 -0
  210. data/lib/insika/stores/memory.rb +123 -0
  211. data/lib/insika/stores/sqlite.rb +183 -0
  212. data/lib/insika/studio/app.rb +1693 -0
  213. data/lib/insika/studio/assets/dist/application.css +1 -0
  214. data/lib/insika/studio/assets/dist/application.js +70 -0
  215. data/lib/insika/studio/forms.rb +335 -0
  216. data/lib/insika/studio/nav_icons.rb +31 -0
  217. data/lib/insika/studio/views/_message.erb +44 -0
  218. data/lib/insika/studio/views/agent_detail.erb +285 -0
  219. data/lib/insika/studio/views/agents.erb +63 -0
  220. data/lib/insika/studio/views/approvals.erb +41 -0
  221. data/lib/insika/studio/views/chats.erb +34 -0
  222. data/lib/insika/studio/views/evals.erb +83 -0
  223. data/lib/insika/studio/views/home.erb +72 -0
  224. data/lib/insika/studio/views/layout.erb +94 -0
  225. data/lib/insika/studio/views/login.erb +17 -0
  226. data/lib/insika/studio/views/mcp.erb +91 -0
  227. data/lib/insika/studio/views/not_found.erb +5 -0
  228. data/lib/insika/studio/views/playground.erb +47 -0
  229. data/lib/insika/studio/views/refinement.erb +234 -0
  230. data/lib/insika/studio/views/session.erb +137 -0
  231. data/lib/insika/studio/views/settings.erb +168 -0
  232. data/lib/insika/studio/views/skills.erb +141 -0
  233. data/lib/insika/studio/views/system_files.erb +65 -0
  234. data/lib/insika/studio/views/task.erb +105 -0
  235. data/lib/insika/studio/views/tasks.erb +33 -0
  236. data/lib/insika/studio/views/tool_edit.erb +107 -0
  237. data/lib/insika/studio/views/tools.erb +89 -0
  238. data/lib/insika/subagent_graph.rb +96 -0
  239. data/lib/insika/system_file_store.rb +96 -0
  240. data/lib/insika/task_actor.rb +128 -0
  241. data/lib/insika/task_store.rb +250 -0
  242. data/lib/insika/telemetry/pricing.rb +104 -0
  243. data/lib/insika/telemetry/recorder.rb +228 -0
  244. data/lib/insika/telemetry.rb +127 -0
  245. data/lib/insika/testing/store_contract.rb +270 -0
  246. data/lib/insika/tick.rb +122 -0
  247. data/lib/insika/token_estimator.rb +16 -0
  248. data/lib/insika/token_store.rb +168 -0
  249. data/lib/insika/tool_assembly.rb +140 -0
  250. data/lib/insika/tool_catalog.rb +89 -0
  251. data/lib/insika/tool_definition.rb +518 -0
  252. data/lib/insika/tool_envelope.rb +140 -0
  253. data/lib/insika/tool_manifest.rb +218 -0
  254. data/lib/insika/tool_output_compressor.rb +100 -0
  255. data/lib/insika/tool_registry.rb +21 -0
  256. data/lib/insika/tool_store.rb +135 -0
  257. data/lib/insika/tool_trace_store.rb +92 -0
  258. data/lib/insika/tools/a2a_remote.rb +48 -0
  259. data/lib/insika/tools/agent_enum.rb +68 -0
  260. data/lib/insika/tools/concurrency.rb +54 -0
  261. data/lib/insika/tools/data_defined_tool.rb +219 -0
  262. data/lib/insika/tools/load_skill.rb +99 -0
  263. data/lib/insika/tools/remember.rb +53 -0
  264. data/lib/insika/tools/stuck_signal.rb +44 -0
  265. data/lib/insika/tools/subagent.rb +75 -0
  266. data/lib/insika/tools/subagents.rb +77 -0
  267. data/lib/insika/tools/tool_search.rb +94 -0
  268. data/lib/insika/turn_output.rb +139 -0
  269. data/lib/insika/turn_state.rb +162 -0
  270. data/lib/insika/turn_timing.rb +56 -0
  271. data/lib/insika/usage_ledger.rb +47 -0
  272. data/lib/insika/version.rb +3 -1
  273. data/lib/insika/wiring/graph.rb +249 -0
  274. data/lib/insika/workflow.rb +185 -0
  275. data/lib/insika/workflow_registry.rb +33 -0
  276. data/lib/insika.rb +220 -4
  277. 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