insika 0.3.0 → 0.7.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 (190) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +180 -0
  3. data/README.md +45 -10
  4. data/bin/insika +684 -0
  5. data/bin/insika-router +87 -0
  6. data/docs/AGENTS.md +94 -403
  7. data/docs/API.md +5 -5
  8. data/docs/ARCHITECTURE.md +3 -2
  9. data/docs/ARTIFACTS.md +95 -0
  10. data/docs/BENCHMARK.md +2 -2
  11. data/docs/CHANNELS.md +14 -14
  12. data/docs/CONTEXT.md +9 -7
  13. data/docs/DEMO.md +80 -0
  14. data/docs/DEPLOY.md +71 -3
  15. data/docs/EMBEDDING.md +1 -1
  16. data/docs/EVALS.md +128 -3
  17. data/docs/FACTS.md +3 -3
  18. data/docs/HARVEST.md +5 -6
  19. data/docs/KNOWLEDGE.md +290 -0
  20. data/docs/LOADTEST.md +2 -2
  21. data/docs/MEDIA.md +128 -0
  22. data/docs/OBSERVABILITY.md +15 -10
  23. data/docs/OUTCOMES.md +137 -0
  24. data/docs/PLUGINS.md +51 -6
  25. data/docs/POLICY.md +216 -0
  26. data/docs/REFINEMENT.md +14 -9
  27. data/docs/RELEASING.md +4 -4
  28. data/docs/ROUTER.md +213 -0
  29. data/docs/RUNNING-LOCAL.md +3 -3
  30. data/docs/SCHEDULING.md +121 -0
  31. data/docs/SECURITY.md +22 -6
  32. data/docs/SKILLS.md +11 -2
  33. data/docs/SOAK.md +2 -2
  34. data/docs/TEMPLATES.md +134 -0
  35. data/docs/TOOLS.md +152 -27
  36. data/docs/WHY.md +1 -1
  37. data/docs/WORKFLOWS.md +2 -2
  38. data/docs/_includes/head_custom.html +5 -0
  39. data/docs/_includes/title.html +13 -0
  40. data/docs/_sass/color_schemes/insika.scss +32 -0
  41. data/docs/_sass/custom/custom.scss +199 -0
  42. data/docs/_sass/custom/setup.scss +26 -0
  43. data/docs/assets/img/favicon.svg +7 -0
  44. data/docs/assets/img/insika-mark.svg +7 -0
  45. data/docs/core-concepts.md +21 -0
  46. data/docs/domain.md +4 -4
  47. data/docs/improve.md +20 -0
  48. data/docs/index.md +8 -5
  49. data/docs/integrate.md +20 -0
  50. data/docs/operate.md +13 -6
  51. data/docs/prompts/ADD-TOOL.md +118 -0
  52. data/docs/prompts/DIAGNOSE-TURN.md +65 -0
  53. data/docs/prompts/GO-LIVE.md +138 -0
  54. data/docs/prompts/RUN-EXAMPLES.md +70 -0
  55. data/docs/reference.md +19 -0
  56. data/docs/ship.md +10 -2
  57. data/docs/start-here.md +18 -0
  58. data/lib/insika/agent_profile.rb +73 -16
  59. data/lib/insika/artifact_signing.rb +82 -0
  60. data/lib/insika/artifact_store.rb +160 -0
  61. data/lib/insika/channel_delivery.rb +1 -1
  62. data/lib/insika/chat_builder.rb +22 -2
  63. data/lib/insika/commands/agent_payload.rb +2 -2
  64. data/lib/insika/commands/backfill_knowledge.rb +145 -0
  65. data/lib/insika/commands/delete_artifact.rb +35 -0
  66. data/lib/insika/commands/delete_concept.rb +34 -0
  67. data/lib/insika/commands/delete_mcp.rb +6 -2
  68. data/lib/insika/commands/delete_tenant_data.rb +15 -3
  69. data/lib/insika/commands/gate_refinement.rb +1 -1
  70. data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
  71. data/lib/insika/commands/restore_concept.rb +34 -0
  72. data/lib/insika/commands/seed_demo_data.rb +31 -0
  73. data/lib/insika/commands/upsert_mcp.rb +6 -3
  74. data/lib/insika/commands/write_concept.rb +57 -0
  75. data/lib/insika/context/priority.rb +2 -0
  76. data/lib/insika/context/providers/knowledge.rb +108 -0
  77. data/lib/insika/context/providers/prompt.rb +30 -24
  78. data/lib/insika/cron.rb +189 -0
  79. data/lib/insika/demo/agent_attrs.rb +43 -0
  80. data/lib/insika/demo/golden_cases.rb +81 -0
  81. data/lib/insika/demo/seeder.rb +336 -0
  82. data/lib/insika/doctor.rb +176 -8
  83. data/lib/insika/dsl/definition.rb +3 -2
  84. data/lib/insika/dsl/runtime.rb +60 -79
  85. data/lib/insika/dsl/server_boot.rb +23 -1
  86. data/lib/insika/dsl/system.rb +10 -2
  87. data/lib/insika/dsl.rb +103 -2
  88. data/lib/insika/env_schema.rb +16 -1
  89. data/lib/insika/evals/golden.rb +41 -4
  90. data/lib/insika/evals/judge.rb +47 -2
  91. data/lib/insika/evals/pairwise.rb +11 -0
  92. data/lib/insika/evals/persona.rb +98 -0
  93. data/lib/insika/evals/runner.rb +9 -0
  94. data/lib/insika/evals/simulator.rb +225 -0
  95. data/lib/insika/evals/transport.rb +83 -1
  96. data/lib/insika/event_stream.rb +10 -0
  97. data/lib/insika/executor.rb +231 -55
  98. data/lib/insika/followup_policy.rb +2 -25
  99. data/lib/insika/golden_store.rb +16 -1
  100. data/lib/insika/grounding/matcher.rb +1 -1
  101. data/lib/insika/knowledge.rb +680 -0
  102. data/lib/insika/knowledge_store.rb +140 -0
  103. data/lib/insika/mcp_client.rb +94 -0
  104. data/lib/insika/mcp_json.rb +74 -0
  105. data/lib/insika/mcp_live_tool.rb +43 -0
  106. data/lib/insika/mcp_store.rb +98 -26
  107. data/lib/insika/mcp_tool_ingestor.rb +30 -8
  108. data/lib/insika/mcp_tool_registry.rb +100 -0
  109. data/lib/insika/media.rb +115 -31
  110. data/lib/insika/message_origin.rb +1 -1
  111. data/lib/insika/middleware.rb +9 -0
  112. data/lib/insika/onboarding.rb +17 -1
  113. data/lib/insika/outcome_store.rb +1 -1
  114. data/lib/insika/overlay_tool_registry.rb +37 -17
  115. data/lib/insika/packaging.rb +2 -2
  116. data/lib/insika/profile_source.rb +8 -1
  117. data/lib/insika/prompt_catalog.rb +10 -0
  118. data/lib/insika/retention.rb +36 -1
  119. data/lib/insika/router/app.rb +157 -0
  120. data/lib/insika/router/backend_pool.rb +98 -0
  121. data/lib/insika/router/hash_ring.rb +55 -0
  122. data/lib/insika/router/proxy_body.rb +34 -0
  123. data/lib/insika/router/session_key.rb +54 -0
  124. data/lib/insika/router.rb +18 -0
  125. data/lib/insika/schedule.rb +177 -0
  126. data/lib/insika/schedule_engine.rb +314 -0
  127. data/lib/insika/schedule_store.rb +208 -0
  128. data/lib/insika/server/app.rb +105 -15
  129. data/lib/insika/server/rack_app.rb +5 -1
  130. data/lib/insika/server/responses.rb +1 -1
  131. data/lib/insika/skill_catalog.rb +12 -0
  132. data/lib/insika/steer_injector.rb +21 -10
  133. data/lib/insika/studio/app.rb +567 -45
  134. data/lib/insika/studio/assets/dist/application.css +1 -1
  135. data/lib/insika/studio/assets/dist/application.js +21 -21
  136. data/lib/insika/studio/forms.rb +46 -5
  137. data/lib/insika/studio/nav_icons.rb +14 -1
  138. data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
  139. data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
  140. data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
  141. data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
  142. data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
  143. data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
  144. data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
  145. data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
  146. data/lib/insika/studio/views/_agents_master.erb +44 -0
  147. data/lib/insika/studio/views/_message.erb +49 -32
  148. data/lib/insika/studio/views/agent_detail.erb +61 -820
  149. data/lib/insika/studio/views/agents.erb +70 -57
  150. data/lib/insika/studio/views/artifact.erb +23 -0
  151. data/lib/insika/studio/views/artifacts.erb +59 -0
  152. data/lib/insika/studio/views/evals.erb +2 -2
  153. data/lib/insika/studio/views/facts.erb +1 -1
  154. data/lib/insika/studio/views/funnel.erb +1 -1
  155. data/lib/insika/studio/views/home.erb +106 -67
  156. data/lib/insika/studio/views/knowledge.erb +123 -0
  157. data/lib/insika/studio/views/layout.erb +14 -11
  158. data/lib/insika/studio/views/mcp.erb +174 -80
  159. data/lib/insika/studio/views/session.erb +231 -177
  160. data/lib/insika/studio/views/settings.erb +39 -1
  161. data/lib/insika/studio/views/skills.erb +1 -1
  162. data/lib/insika/studio/views/tools.erb +24 -9
  163. data/lib/insika/templates/browser-agent/README.md +36 -0
  164. data/lib/insika/templates/browser-agent/agent.rb +49 -0
  165. data/lib/insika/templates/daily-digest/README.md +38 -0
  166. data/lib/insika/templates/daily-digest/agent.rb +77 -0
  167. data/lib/insika/templates/repo-explorer/README.md +36 -0
  168. data/lib/insika/templates/repo-explorer/agent.rb +45 -0
  169. data/lib/insika/templates/research-analyst/README.md +26 -0
  170. data/lib/insika/templates/research-analyst/agent.rb +58 -0
  171. data/lib/insika/templates/review-panel/README.md +20 -0
  172. data/lib/insika/templates/review-panel/agent.rb +50 -0
  173. data/lib/insika/templates/travel-planner/README.md +35 -0
  174. data/lib/insika/templates/travel-planner/agent.rb +87 -0
  175. data/lib/insika/templates.rb +112 -0
  176. data/lib/insika/tick.rb +24 -12
  177. data/lib/insika/timezone.rb +45 -0
  178. data/lib/insika/tools/generate_image.rb +52 -7
  179. data/lib/insika/tools/load_knowledge.rb +74 -0
  180. data/lib/insika/tools/run_persona_eval.rb +328 -0
  181. data/lib/insika/tools/save_artifact.rb +95 -0
  182. data/lib/insika/turn_output.rb +1 -1
  183. data/lib/insika/turn_state.rb +15 -4
  184. data/lib/insika/version.rb +1 -1
  185. data/lib/insika/wiring/graph.rb +184 -12
  186. data/lib/insika/wiring/graph_chat.rb +102 -0
  187. data/lib/insika.rb +57 -0
  188. metadata +105 -5
  189. data/docs/build.md +0 -14
  190. data/docs/understand.md +0 -10
@@ -15,7 +15,7 @@ module Insika
15
15
  def initialize(tool_registry:, skill_catalog:, checkpoint_store:, event_stream:,
16
16
  hooks:, tool_catalog: nil, memory_store: nil, subagent_runner: nil,
17
17
  tool_trace_store: nil, media_runner: nil, session_store: nil,
18
- contact_store: nil, followup_store: nil)
18
+ contact_store: nil, followup_store: nil, knowledge_store: nil)
19
19
  @tool_registry = tool_registry
20
20
  @skill_catalog = skill_catalog
21
21
  @checkpoint_store = checkpoint_store
@@ -23,6 +23,10 @@ module Insika
23
23
  @hooks = hooks
24
24
  @tool_catalog = tool_catalog
25
25
  @memory_store = memory_store
26
+ # load_knowledge is the knowledge-read system tool — wired only with
27
+ # @knowledge_store present AND profile.knowledge["retrieve"] (a double
28
+ # gate, like remember). nil = never wired (parity).
29
+ @knowledge_store = knowledge_store
26
30
  # only to trace load_skill, which is not enveloped — nil = no trace (parity).
27
31
  @tool_trace_store = tool_trace_store
28
32
  # the object exposing #run_subagent (the Executor). nil = the
@@ -104,6 +108,15 @@ module Insika
104
108
  event_stream: @event_stream, state: state)
105
109
  end
106
110
 
111
+ # load_knowledge is a system default (outside the allowlist), like
112
+ # load_skill — wired only with @knowledge_store present AND the
113
+ # profile's opt-in (double gate, like remember). Reads the same
114
+ # (agent, tenant) scope the Knowledge context provider searches.
115
+ if @knowledge_store && Coercion.truthy?(state.profile.knowledge&.dig("retrieve"))
116
+ tools << Tools::LoadKnowledge.new(@knowledge_store, state.profile.id, tenant: state.tenant,
117
+ trace_recorder: @tool_trace_store, state: state)
118
+ end
119
+
107
120
  # update_briefing / set_next_step are the briefing-write system tools
108
121
  # — wired only with @session_store present AND
109
122
  # profile.briefing_fields non-empty (double gate, like remember). Never
@@ -362,9 +375,16 @@ module Insika
362
375
  # under concurrency `after_tool_result` labelled every result with whichever
363
376
  # call started last.
364
377
  state.current_tool_name = tool_call.name.to_s
365
- if state.current_tool_name == "load_skill"
378
+ case state.current_tool_name
379
+ when "load_skill"
366
380
  args = tool_call.arguments || {}
367
381
  emit.call(:skill_activated, { name: args["name"] || args[:name] })
382
+ when "load_knowledge"
383
+ # The adoption risk is the model never calling this tool, not what
384
+ # it injects — so the metric here is retrieval CALLS, tracked at
385
+ # the same point :skill_activated is.
386
+ args = tool_call.arguments || {}
387
+ emit.call(:knowledge_retrieved, { name: args["name"] || args[:name], agent: state.profile.id })
368
388
  else
369
389
  emit.call(:tool_call, { name: tool_call.name, arguments: tool_call.arguments })
370
390
  end
@@ -13,9 +13,9 @@ module Insika
13
13
  tools_allow_groups skills skills_eager context_providers workflows_allow policies
14
14
  prompt_refs limits approvals_required capabilities subagents tools_deferred
15
15
  memory prompt_caching tool_persistence tool_output_compression budget reliability alerts
16
- routes stuck_signal outputs briefing_fields grounding funnel followup
16
+ routes stuck_signal outputs stt_prompt briefing_fields grounding funnel followup
17
17
  params model_policy guardrails refinement capabilities_declared
18
- edge_stream metadata distill harvest].freeze
18
+ edge_stream metadata distill harvest knowledge schedules].freeze
19
19
 
20
20
  module_function
21
21
 
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Insika
6
+ module Commands
7
+ # The recovery path (extraction is best-effort — a crash between a turn's
8
+ # terminal and the write loses that turn's concepts, not the conversation
9
+ # nor a previously learned one). Replays one agent's stored sessions
10
+ # through the SAME `Knowledge::Extractor` the per-turn hook uses.
11
+ #
12
+ # Synchronous — the CLI's own path (`insika knowledge:backfill`), same
13
+ # "no app boot" discipline as RunHarvest/RunDistillation: no task, no
14
+ # turn, nothing written but the concepts themselves.
15
+ class BackfillKnowledge
16
+ DEFAULT_MIN_MESSAGES = 2
17
+
18
+ def initialize(profiles:, knowledge_store:, session_store:, task_store:,
19
+ settings_store: nil, event_stream:, extractor_factory: nil, consolidator_factory: nil)
20
+ @profiles = ProfileSource.coerce(profiles)
21
+ @knowledge_store = knowledge_store
22
+ @session_store = session_store
23
+ @task_store = task_store
24
+ @settings_store = settings_store
25
+ @extractor_factory = extractor_factory ||
26
+ ->(config) { Knowledge::ExtractorFactory.build(config, utility_model: utility_model) }
27
+ @consolidator_factory = consolidator_factory ||
28
+ ->(config) { Knowledge::ConsolidatorFactory.build(config, utility_model: utility_model) }
29
+ @event_stream = event_stream
30
+ end
31
+
32
+ # payload: { agent:, since?: ISO8601 }
33
+ # -> { backfilled: true, sessions: N, concepts: N, conflicts: N, dropped: {...} }
34
+ # | { backfilled: false, skipped: "disabled|no_model|no_sessions" }
35
+ def call(command)
36
+ p = AgentPayload.symbolize(command.payload)
37
+ agent = AgentPayload.presence(p[:agent])
38
+ raise Insika::ValidationError, "agent is required" if agent.nil?
39
+
40
+ profile = @profiles[agent] || (raise Insika::NotFoundError, "agent '#{agent}' not configured")
41
+ config = Coercion.deep_stringify(profile.knowledge)
42
+ return skip("disabled") if config.nil? || !Coercion.truthy?(config["extract"])
43
+
44
+ extractor = @extractor_factory.call(config)
45
+ return skip("no_model") if extractor.nil?
46
+
47
+ # Same resolved model as the extractor. nil (unresolvable) is still
48
+ # meaningful: Knowledge.write_concept's conservative default.
49
+ consolidator = @consolidator_factory.call(config)
50
+
51
+ sessions = resolve_sessions(agent, p)
52
+ return skip("no_sessions") if sessions.empty?
53
+
54
+ concepts = 0
55
+ conflicts = 0
56
+ dropped = Hash.new(0)
57
+ sessions.each do |s|
58
+ result = extractor.extract(prompt: build_prompt(config, s[:messages]))
59
+ result[:concepts].each do |c|
60
+ case write_concept(profile, s[:id], c, consolidator)[:verdict]
61
+ when :new, :related then concepts += 1
62
+ when :contradicting then conflicts += 1
63
+ end
64
+ end
65
+ result[:dropped].each { |k, v| dropped[k] += v }
66
+ end
67
+
68
+ emit(:knowledge_backfilled, agent: agent, sessions: sessions.size, concepts: concepts,
69
+ conflicts: conflicts, dropped: dropped.to_h)
70
+ { backfilled: true, sessions: sessions.size, concepts: concepts, conflicts: conflicts, dropped: dropped.to_h }
71
+ end
72
+
73
+ private
74
+
75
+ def skip(reason) = { backfilled: false, skipped: reason }
76
+
77
+ # Every task of the agent, distinct session ids, newest first — the
78
+ # same reading `RunHarvest#window_session_ids` uses, without the
79
+ # last-N cap (a backfill is meant to be exhaustive over `--since`).
80
+ def resolve_sessions(agent, p)
81
+ since = AgentPayload.presence(p[:since])
82
+ tasks = @task_store.each_id.filter_map { |id| @task_store.find(id) }
83
+ .select { |t| task_agent(t) == agent }
84
+ .sort_by { |t| [t.created_at.to_s, t.id] }.reverse
85
+ tasks = tasks.select { |t| t.created_at.to_s >= since.to_s } if since
86
+
87
+ seen = {}
88
+ tasks.filter_map do |t|
89
+ sid = Coercion.presence(t.session_id)
90
+ next if sid.nil? || seen[sid]
91
+
92
+ seen[sid] = true
93
+ session = @session_store.find(sid)
94
+ next unless session && session.messages.size >= DEFAULT_MIN_MESSAGES
95
+
96
+ { id: sid.to_s, messages: session.messages }
97
+ end
98
+ end
99
+
100
+ def task_agent(task)
101
+ task.command.is_a?(Hash) ? task.command.dig("payload", "agent").to_s : ""
102
+ end
103
+
104
+ def build_prompt(config, messages)
105
+ base = Coercion.presence(config["prompt"]) || Knowledge::DEFAULT_PROMPT
106
+ <<~PROMPT
107
+ #{base.rstrip}
108
+
109
+ ## The conversation
110
+
111
+ #{render_transcript(messages)}
112
+ PROMPT
113
+ end
114
+
115
+ def render_transcript(messages)
116
+ redacted, = Insika::Safety::Detectors.redact(
117
+ messages.each_with_index.map { |m, i| "[#{i}] #{m['role']}: #{m['content']}" }.join("\n")
118
+ )
119
+ redacted
120
+ end
121
+
122
+ def write_concept(profile, session_id, concept, consolidator)
123
+ outcome = Knowledge.write_concept(store: @knowledge_store, agent_id: profile.id, concept: concept,
124
+ session_id: session_id, consolidator: consolidator)
125
+ case outcome[:verdict]
126
+ when :new, :related
127
+ emit(:knowledge_learned, name: outcome[:name], type: outcome[:type], agent: profile.id)
128
+ when :contradicting
129
+ emit(:knowledge_conflict, name: outcome[:name], agent: profile.id)
130
+ end
131
+ outcome
132
+ end
133
+
134
+ def utility_model
135
+ return nil unless @settings_store
136
+
137
+ @settings_store.get["utility_model"]
138
+ end
139
+
140
+ def emit(type, **data)
141
+ @event_stream.emit(Insika::Event.new(type: type, data: data, meta: { at: Time.now.utc.iso8601 }))
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Insika
6
+ module Commands
7
+ # The Studio's delete on the Artifacts tab — a bus command, like every
8
+ # Studio mutation (the Studio never writes a store directly). Idempotent:
9
+ # an unknown id is a no-op, not an error.
10
+ class DeleteArtifact
11
+ def initialize(artifact_store:, event_stream:)
12
+ @artifact_store = artifact_store
13
+ @event_stream = event_stream
14
+ end
15
+
16
+ # -> { deleted: id } | { deleted: nil } (unknown id).
17
+ def call(command)
18
+ id = Coercion.presence(command.payload[:id] || command.payload["id"])
19
+ raise ValidationError, "id is required" if id.nil?
20
+
21
+ deleted = @artifact_store.delete(id.to_s)
22
+ if deleted
23
+ @event_stream.emit(Insika::Event.new(
24
+ type: :artifact_deleted,
25
+ data: { id: id.to_s },
26
+ meta: { at: Time.now.utc.iso8601 }
27
+ ))
28
+ { deleted: id.to_s }
29
+ else
30
+ { deleted: nil }
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Insika
6
+ module Commands
7
+ # Control command: removes a learned concept from the KnowledgeStore.
8
+ # -> { name, agent, tenant, deleted: true }.
9
+ class DeleteConcept
10
+ def initialize(knowledge_store:, event_stream:)
11
+ @knowledge_store = knowledge_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
+ agent = AgentPayload.presence(p[:agent])
19
+ tenant = AgentPayload.presence(p[:tenant])
20
+ raise Insika::ValidationError, "name is required" if name.nil?
21
+ raise Insika::ValidationError, "agent is required" if agent.nil?
22
+
23
+ deleted = @knowledge_store.delete(agent, name, tenant: tenant)
24
+ raise Insika::NotFoundError, "concept '#{name}' not found for agent '#{agent}'" unless deleted
25
+
26
+ @event_stream.emit(Insika::Event.new(
27
+ type: :knowledge_deleted, data: { name: name, agent: agent },
28
+ meta: { at: Time.now.utc.iso8601 }
29
+ ))
30
+ { name: name, agent: agent, tenant: tenant, deleted: true }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -5,11 +5,14 @@ require "time"
5
5
  module Insika
6
6
  module Commands
7
7
  # Control command: removes an MCP instance from the
8
- # McpStore. Idempotent (`existed: false` if there was none). -> { existed: bool }.
8
+ # McpStore. Idempotent (`existed: false` if there was none). Evicts any
9
+ # memoized client for this name — a removed instance stops answering
10
+ # tool calls right away, not after a restart. -> { existed: bool }.
9
11
  class DeleteMcp
10
- def initialize(mcp_store:, event_stream:)
12
+ def initialize(mcp_store:, event_stream:, mcp_registry: nil)
11
13
  @mcp_store = mcp_store
12
14
  @event_stream = event_stream
15
+ @mcp_registry = mcp_registry
13
16
  end
14
17
 
15
18
  def call(command)
@@ -18,6 +21,7 @@ module Insika
18
21
  raise Insika::ValidationError, "name is required" if name.nil?
19
22
 
20
23
  existed = @mcp_store.delete(name)
24
+ @mcp_registry&.evict(name)
21
25
  @event_stream.emit(Insika::Event.new(
22
26
  type: :mcp_deleted,
23
27
  data: { name: name, existed: existed },
@@ -21,9 +21,9 @@ module Insika
21
21
  context_trace_store: nil, model_visible_trace_store: nil,
22
22
  outcome_store: nil, task_store: nil,
23
23
  checkpoint_store: nil, outbox_store: nil, shadow_pairs: nil,
24
- token_store: nil, funnel_store: nil, event_stream:,
24
+ token_store: nil, funnel_store: nil, event_stream:,
25
25
  followup_store: nil, contact_store: nil, proposal_store: nil,
26
- harvest_store: nil)
26
+ harvest_store: nil, schedule_store: nil, artifact_store: nil)
27
27
  @memory_store = memory_store
28
28
  @token_store = token_store
29
29
  @session_store = session_store
@@ -40,6 +40,8 @@ module Insika
40
40
  @contact_store = contact_store # ; nil = nothing to sweep
41
41
  @proposal_store = proposal_store # ; nil = nothing to sweep
42
42
  @harvest_store = harvest_store # ; nil = nothing to sweep
43
+ @schedule_store = schedule_store # ; nil = nothing to sweep
44
+ @artifact_store = artifact_store # ; nil = nothing to sweep
43
45
  @event_stream = event_stream
44
46
  end
45
47
 
@@ -72,6 +74,12 @@ module Insika
72
74
  # candidates reference sessions, and sessions carry the
73
75
  # tenant prefix — the harvest rows die with the tenant (D11).
74
76
  harvest = @harvest_store ? @harvest_store.purge(tenant: tenant) : 0
77
+ # recurring-schedule rows die with the tenant too (their
78
+ # message text is content, never kept behind an offboarded tenant).
79
+ schedules = @schedule_store ? @schedule_store.purge(tenant: tenant) : 0
80
+ # artifacts die with the tenant — a report is content, and the
81
+ # tenant binding is the isolation boundary (never a model-typed id).
82
+ artifacts = @artifact_store ? @artifact_store.purge(tenant: tenant) : 0
75
83
 
76
84
  @event_stream.emit(Insika::Event.new(
77
85
  type: :tenant_data_deleted,
@@ -83,12 +91,16 @@ module Insika
83
91
  contacts: contacts,
84
92
  proposals: proposals,
85
93
  harvest: harvest,
94
+ schedules: schedules,
95
+ artifacts: artifacts,
86
96
  tokens_revoked: tokens_revoked }.merge(purged),
87
97
  meta: { at: Time.now.utc.iso8601 }
88
98
  ))
89
99
  { tenant: tenant, sessions: sessions, memory_records: memory_records,
90
100
  outcomes: outcomes, funnel: funnel, followups: followups, contacts: contacts,
91
- proposals: proposals, harvest: harvest, tokens_revoked: tokens_revoked }.merge(purged)
101
+ proposals: proposals, harvest: harvest, schedules: schedules,
102
+ artifacts: artifacts,
103
+ tokens_revoked: tokens_revoked }.merge(purged)
92
104
  end
93
105
  end
94
106
  end
@@ -12,7 +12,7 @@ module Insika
12
12
  # Synchronous like `run_refinement`, and for the same reason: it is operator-paced
13
13
  # work with a human waiting on the answer. It is NOT cheap — the replay is a real
14
14
  # conversation per golden case — so it is fired deliberately, never on a timer
15
- # (the engine has no scheduler and this design does not add one).
15
+ # (a recurring timer may schedule OTHER turns; this gate stays human-paced).
16
16
  #
17
17
  # Payload:
18
18
  # run_id (required) a run in :completed — the evidence the candidate answers
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Insika
6
+ module Commands
7
+ # Control command: LIVE re-discovery of an MCP instance's tools.
8
+ # Delegates to McpToolRegistry#refresh (connect +
9
+ # tools/list + write McpStore#tools_cache) — TOOL EXECUTION never reads
10
+ # that cache; this command only refreshes what `entries`/Studio/doctor
11
+ # display. The CLI verb / API route / Studio button that call it are
12
+ # later work (PR3/PR4) — the command itself is the seam they'll share.
13
+ # -> { instance:, tools: [{name,description,inputSchema}, ...] }.
14
+ class RefreshMcpTools
15
+ def initialize(mcp_registry:, event_stream:)
16
+ @mcp_registry = mcp_registry
17
+ @event_stream = event_stream
18
+ end
19
+
20
+ def call(command)
21
+ p = symbolize(command.payload)
22
+ name = Insika::Coercion.presence(p[:name]) ||
23
+ (raise Insika::ValidationError, "refresh_mcp_tools: 'name' (MCP instance) is required")
24
+
25
+ tools = @mcp_registry.refresh(name)
26
+ emit(name, tools)
27
+ { instance: name, tools: tools }
28
+ end
29
+
30
+ private
31
+
32
+ # Emits only the COUNT + the instance name (never a tool's schema, in
33
+ # case a schema description ever carries something sensitive): 0 leakage.
34
+ def emit(name, tools)
35
+ @event_stream.emit(Insika::Event.new(
36
+ type: :mcp_tools_refreshed,
37
+ data: { instance: name, tools: tools.size },
38
+ meta: { at: Time.now.utc.iso8601 }
39
+ ))
40
+ end
41
+
42
+ def symbolize(payload)
43
+ (payload || {}).each_with_object({}) { |(k, v), acc| acc[k.to_sym] = v }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Insika
6
+ module Commands
7
+ # Control command: restores an old version of a concept as the current
8
+ # content (a new write — linear history, like `RestoreSystemFile`).
9
+ # -> { name, agent, tenant, updated_at }.
10
+ class RestoreConcept
11
+ def initialize(knowledge_store:, event_stream:)
12
+ @knowledge_store = knowledge_store
13
+ @event_stream = event_stream
14
+ end
15
+
16
+ def call(command)
17
+ p = AgentPayload.symbolize(command.payload)
18
+ name = AgentPayload.presence(p[:name])
19
+ agent = AgentPayload.presence(p[:agent])
20
+ tenant = AgentPayload.presence(p[:tenant])
21
+ raise Insika::ValidationError, "name is required" if name.nil?
22
+ raise Insika::ValidationError, "agent is required" if agent.nil?
23
+ raise Insika::ValidationError, "version is required" if p[:version].nil?
24
+
25
+ rec = @knowledge_store.restore(agent, name, p[:version], tenant: tenant)
26
+ @event_stream.emit(Insika::Event.new(
27
+ type: :knowledge_learned, data: { name: name, agent: agent },
28
+ meta: { at: Time.now.utc.iso8601 }
29
+ ))
30
+ { name: name, agent: agent, tenant: tenant, updated_at: rec["updated_at"] }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Insika
6
+ module Commands
7
+ # Studio/CLI-facing adapter over Insika::Demo::Seeder — the one path both
8
+ # `insika demo:seed` and the Studio's "Seed demo data" button use (same
9
+ # discipline as RunDistillation: one object, two front doors).
10
+ class SeedDemoData
11
+ def initialize(seeder:, event_stream:)
12
+ @seeder = seeder
13
+ @event_stream = event_stream
14
+ end
15
+
16
+ # payload: { force: bool } -> the seeder's result Hash.
17
+ def call(command)
18
+ force = Coercion.truthy?(command.payload[:force] || command.payload["force"])
19
+ result = @seeder.seed!(force: force)
20
+ if result[:seeded]
21
+ @event_stream.emit(Insika::Event.new(
22
+ type: :demo_data_seeded,
23
+ data: { agent: result[:agent], counts: result[:counts] },
24
+ meta: { at: Time.now.utc.iso8601 }
25
+ ))
26
+ end
27
+ result
28
+ end
29
+ end
30
+ end
31
+ end
@@ -8,17 +8,20 @@ module Insika
8
8
  # (transport/command/url/enabled + `env` credentials) in the McpStore. The
9
9
  # credentials are sentinel-aware per key (__OCULTO__ preserves; "" clears;
10
10
  # a new string replaces). Returns the MASKED record (env never comes back in
11
- # plaintext). Durable config CRUD — running an MCP client against the
12
- # instance is later runtime.
11
+ # plaintext). Evicts any memoized client for this name (mcp_registry is
12
+ # optional so a bare CLI/store-only caller need not wire one) so an edited
13
+ # command/url/env takes effect on the instance's next call — no restart.
13
14
  class UpsertMcp
14
- def initialize(mcp_store:, event_stream:)
15
+ def initialize(mcp_store:, event_stream:, mcp_registry: nil)
15
16
  @mcp_store = mcp_store
16
17
  @event_stream = event_stream
18
+ @mcp_registry = mcp_registry
17
19
  end
18
20
 
19
21
  def call(command)
20
22
  p = AgentPayload.symbolize(command.payload)
21
23
  masked = @mcp_store.upsert(p) # validates `name`; returns masked
24
+ @mcp_registry&.evict(masked["name"])
22
25
 
23
26
  @event_stream.emit(Insika::Event.new(
24
27
  type: :mcp_upserted,
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Insika
6
+ module Commands
7
+ # Control command: writes a concept (complete concept markdown) into the
8
+ # KnowledgeStore. Validates the frontmatter (a `name` is required) before
9
+ # writing — same discipline as `WriteSkill`. The STORE POSITION (the
10
+ # `agent`/`name`/`tenant` triple) is the identity, exactly like a skill:
11
+ # an operator editing the raw markdown may leave the frontmatter `name:`
12
+ # unchanged even while promoting `provenance: observed` to `policy` —
13
+ # that promotion IS this command, no separate "promote" action needed.
14
+ # -> { name, agent, tenant, updated_at }.
15
+ class WriteConcept
16
+ def initialize(knowledge_store:, event_stream:)
17
+ @knowledge_store = knowledge_store
18
+ @event_stream = event_stream
19
+ end
20
+
21
+ def call(command)
22
+ p = AgentPayload.symbolize(command.payload)
23
+ name = AgentPayload.presence(p[:name])
24
+ agent = AgentPayload.presence(p[:agent])
25
+ tenant = AgentPayload.presence(p[:tenant])
26
+ content = p[:content].to_s
27
+ raise Insika::ValidationError, "name is required" if name.nil?
28
+ raise Insika::ValidationError, "agent is required" if agent.nil?
29
+
30
+ parsed = validate_frontmatter!(content)
31
+ rec = @knowledge_store.write(agent, name, content, tenant: tenant)
32
+ emit(parsed[:type], name, agent)
33
+ { name: name, agent: agent, tenant: tenant, updated_at: rec["updated_at"] }
34
+ end
35
+
36
+ private
37
+
38
+ def emit(type, name, agent)
39
+ @event_stream.emit(Insika::Event.new(
40
+ type: :knowledge_learned, data: { name: name, type: type, agent: agent },
41
+ meta: { at: Time.now.utc.iso8601 }
42
+ ))
43
+ end
44
+
45
+ # Mirrors WriteSkill's frontmatter guard: without a parseable `name`,
46
+ # the concept would be unreachable by name once written (the Studio
47
+ # list keys on the store position, but the editor round-trips the
48
+ # content) — fail early, in the Command.
49
+ def validate_frontmatter!(content)
50
+ parsed = Insika::Knowledge::Concept.parse(content)
51
+ raise Insika::ValidationError, "concept missing YAML frontmatter (--- ... ---) or a `name`" if parsed.nil?
52
+
53
+ parsed
54
+ end
55
+ end
56
+ end
57
+ end
@@ -20,6 +20,8 @@ module Insika
20
20
  PROMPT_REF = 90 # Prompt Catalog guardrails/refs (Prompt) — pinned
21
21
  SKILL_BODY = 85 # <active_skill> trigger-matched body (SkillTrigger)
22
22
  SKILL = 80 # <available_skills> level 1 (Skill)
23
+ KNOWLEDGE = 77 # <knowledge> top-K learned concepts (Knowledge) —
24
+ # below curated skills, above a single conversation's memory
23
25
  MEMORY = 75 # <memory> read path (Memory)
24
26
  TOOL_SEARCH = 70 # <available_tools> level 1 (ToolSearch)
25
27
  BRIEFING = 65 # <briefing> session working state (Briefing) — D5: