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,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