insika 0.3.0 → 0.8.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 (204) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +296 -0
  3. data/README.md +48 -12
  4. data/bin/insika +725 -0
  5. data/bin/insika-router +87 -0
  6. data/docs/AGENTS.md +116 -406
  7. data/docs/API.md +5 -5
  8. data/docs/ARCHITECTURE.md +3 -2
  9. data/docs/ARTIFACTS.md +137 -0
  10. data/docs/BENCHMARK.md +2 -2
  11. data/docs/CHANNELS.md +14 -14
  12. data/docs/CONTEXT.md +63 -19
  13. data/docs/DEMO.md +80 -0
  14. data/docs/DEPLOY.md +87 -10
  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 +17 -29
  21. data/docs/MEDIA.md +128 -0
  22. data/docs/OBSERVABILITY.md +46 -12
  23. data/docs/OUTCOMES.md +137 -0
  24. data/docs/PLUGINS.md +51 -6
  25. data/docs/POLICY.md +222 -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 +5 -5
  30. data/docs/SCHEDULING.md +121 -0
  31. data/docs/SECURITY.md +23 -7
  32. data/docs/SKILLS.md +11 -2
  33. data/docs/SOAK.md +3 -3
  34. data/docs/TEMPLATES.md +134 -0
  35. data/docs/TOOLS.md +176 -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 +99 -17
  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 +50 -19
  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/compaction.rb +196 -0
  76. data/lib/insika/context/builder.rb +6 -2
  77. data/lib/insika/context/fragment.rb +4 -1
  78. data/lib/insika/context/priority.rb +8 -0
  79. data/lib/insika/context/providers/briefing.rb +53 -24
  80. data/lib/insika/context/providers/knowledge.rb +108 -0
  81. data/lib/insika/context/providers/prompt.rb +30 -24
  82. data/lib/insika/context/providers/session.rb +46 -10
  83. data/lib/insika/context_trace_store.rb +11 -1
  84. data/lib/insika/cron.rb +189 -0
  85. data/lib/insika/demo/agent_attrs.rb +43 -0
  86. data/lib/insika/demo/golden_cases.rb +81 -0
  87. data/lib/insika/demo/seeder.rb +336 -0
  88. data/lib/insika/doctor.rb +280 -17
  89. data/lib/insika/dsl/definition.rb +3 -2
  90. data/lib/insika/dsl/runtime.rb +64 -79
  91. data/lib/insika/dsl/server_boot.rb +23 -1
  92. data/lib/insika/dsl/system.rb +10 -2
  93. data/lib/insika/dsl.rb +103 -2
  94. data/lib/insika/env_schema.rb +21 -7
  95. data/lib/insika/evals/golden.rb +41 -4
  96. data/lib/insika/evals/judge.rb +47 -2
  97. data/lib/insika/evals/pairwise.rb +11 -0
  98. data/lib/insika/evals/persona.rb +98 -0
  99. data/lib/insika/evals/runner.rb +9 -0
  100. data/lib/insika/evals/simulator.rb +225 -0
  101. data/lib/insika/evals/transport.rb +84 -2
  102. data/lib/insika/event_stream.rb +10 -0
  103. data/lib/insika/executor.rb +295 -55
  104. data/lib/insika/followup_policy.rb +2 -25
  105. data/lib/insika/golden_store.rb +16 -1
  106. data/lib/insika/grounding/matcher.rb +1 -1
  107. data/lib/insika/knowledge.rb +680 -0
  108. data/lib/insika/knowledge_store.rb +140 -0
  109. data/lib/insika/loop_detector.rb +5 -34
  110. data/lib/insika/mcp_client.rb +94 -0
  111. data/lib/insika/mcp_json.rb +74 -0
  112. data/lib/insika/mcp_live_tool.rb +43 -0
  113. data/lib/insika/mcp_store.rb +98 -26
  114. data/lib/insika/mcp_tool_ingestor.rb +30 -8
  115. data/lib/insika/mcp_tool_registry.rb +100 -0
  116. data/lib/insika/media.rb +115 -31
  117. data/lib/insika/message_origin.rb +1 -1
  118. data/lib/insika/middleware.rb +9 -0
  119. data/lib/insika/onboarding.rb +17 -1
  120. data/lib/insika/outcome_store.rb +1 -1
  121. data/lib/insika/overlay_tool_registry.rb +37 -17
  122. data/lib/insika/packaging.rb +2 -2
  123. data/lib/insika/profile_source.rb +15 -1
  124. data/lib/insika/prompt_catalog.rb +10 -0
  125. data/lib/insika/retention.rb +36 -1
  126. data/lib/insika/router/app.rb +157 -0
  127. data/lib/insika/router/backend_pool.rb +98 -0
  128. data/lib/insika/router/hash_ring.rb +55 -0
  129. data/lib/insika/router/proxy_body.rb +34 -0
  130. data/lib/insika/router/session_key.rb +54 -0
  131. data/lib/insika/router.rb +18 -0
  132. data/lib/insika/schedule.rb +177 -0
  133. data/lib/insika/schedule_engine.rb +314 -0
  134. data/lib/insika/schedule_store.rb +208 -0
  135. data/lib/insika/server/app.rb +105 -15
  136. data/lib/insika/server/rack_app.rb +5 -1
  137. data/lib/insika/server/responses.rb +5 -5
  138. data/lib/insika/session_store.rb +34 -4
  139. data/lib/insika/settings_store.rb +8 -1
  140. data/lib/insika/skill_catalog.rb +12 -0
  141. data/lib/insika/soak/runner.rb +4 -4
  142. data/lib/insika/steer_injector.rb +21 -10
  143. data/lib/insika/studio/app.rb +591 -47
  144. data/lib/insika/studio/assets/dist/application.css +1 -1
  145. data/lib/insika/studio/assets/dist/application.js +21 -21
  146. data/lib/insika/studio/forms.rb +57 -5
  147. data/lib/insika/studio/nav_icons.rb +14 -1
  148. data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
  149. data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
  150. data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
  151. data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
  152. data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
  153. data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
  154. data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
  155. data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
  156. data/lib/insika/studio/views/_agents_master.erb +44 -0
  157. data/lib/insika/studio/views/_message.erb +49 -32
  158. data/lib/insika/studio/views/agent_detail.erb +61 -820
  159. data/lib/insika/studio/views/agents.erb +70 -57
  160. data/lib/insika/studio/views/artifact.erb +23 -0
  161. data/lib/insika/studio/views/artifacts.erb +59 -0
  162. data/lib/insika/studio/views/evals.erb +2 -2
  163. data/lib/insika/studio/views/facts.erb +1 -1
  164. data/lib/insika/studio/views/funnel.erb +1 -1
  165. data/lib/insika/studio/views/home.erb +106 -67
  166. data/lib/insika/studio/views/knowledge.erb +123 -0
  167. data/lib/insika/studio/views/layout.erb +14 -11
  168. data/lib/insika/studio/views/mcp.erb +174 -80
  169. data/lib/insika/studio/views/session.erb +231 -177
  170. data/lib/insika/studio/views/settings.erb +50 -1
  171. data/lib/insika/studio/views/skills.erb +1 -1
  172. data/lib/insika/studio/views/tools.erb +24 -9
  173. data/lib/insika/telemetry/recorder.rb +49 -1
  174. data/lib/insika/templates/browser-agent/README.md +36 -0
  175. data/lib/insika/templates/browser-agent/agent.rb +49 -0
  176. data/lib/insika/templates/daily-digest/README.md +47 -0
  177. data/lib/insika/templates/daily-digest/agent.rb +77 -0
  178. data/lib/insika/templates/repo-explorer/README.md +36 -0
  179. data/lib/insika/templates/repo-explorer/agent.rb +45 -0
  180. data/lib/insika/templates/research-analyst/README.md +26 -0
  181. data/lib/insika/templates/research-analyst/agent.rb +68 -0
  182. data/lib/insika/templates/review-panel/README.md +20 -0
  183. data/lib/insika/templates/review-panel/agent.rb +50 -0
  184. data/lib/insika/templates/travel-planner/README.md +35 -0
  185. data/lib/insika/templates/travel-planner/agent.rb +87 -0
  186. data/lib/insika/templates.rb +112 -0
  187. data/lib/insika/tick.rb +24 -12
  188. data/lib/insika/timezone.rb +45 -0
  189. data/lib/insika/tool_batch.rb +67 -0
  190. data/lib/insika/tool_usage_report.rb +162 -0
  191. data/lib/insika/tools/generate_image.rb +52 -7
  192. data/lib/insika/tools/load_knowledge.rb +74 -0
  193. data/lib/insika/tools/run_persona_eval.rb +328 -0
  194. data/lib/insika/tools/save_artifact.rb +95 -0
  195. data/lib/insika/turn_budget.rb +91 -0
  196. data/lib/insika/turn_output.rb +1 -1
  197. data/lib/insika/turn_state.rb +15 -4
  198. data/lib/insika/version.rb +1 -1
  199. data/lib/insika/wiring/graph.rb +184 -12
  200. data/lib/insika/wiring/graph_chat.rb +102 -0
  201. data/lib/insika.rb +64 -0
  202. metadata +109 -5
  203. data/docs/build.md +0 -14
  204. data/docs/understand.md +0 -10
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "graph_chat"
4
+
3
5
  module Insika
4
6
  module Wiring
5
7
  # SHARED composition core for both roots: the minimal wiring
@@ -78,11 +80,21 @@ module Insika
78
80
  # followup) so the engine, the tools and the Studio always have stores.
79
81
  contact_store = Insika::ContactStore.new(store: backend)
80
82
  followup_store = Insika::FollowupStore.new(store: backend)
83
+ # the recurring-schedule rows: one row per
84
+ # declared schedule, declaration + runtime state. Built unconditionally
85
+ # (empty and free when no pack declares schedules) so the engine, the
86
+ # doctor and the Studio always have a store.
87
+ schedule_store = Insika::ScheduleStore.new(store: backend)
81
88
  # the distilled proposals + the latched dedup ledger +
82
89
  # the per-session markers. Built unconditionally (empty and free when
83
90
  # no pack declares distill) so the wiki, the engine and the LGPD
84
91
  # purges always have a store.
85
92
  proposal_store = Insika::ProposalStore.new(store: backend)
93
+ # concepts extracted from finished turns. Built
94
+ # unconditionally (empty and free when no pack declares knowledge)
95
+ # so the extractor, the Studio and the LGPD purges always have a
96
+ # store.
97
+ knowledge_store = Insika::KnowledgeStore.new(store: backend)
86
98
  # refinement RUNS (reports over real traffic). Runtime data,
87
99
  # same backend as sessions/tasks — the collector and the command that write it
88
100
  # are the root's business (deployment-only, like the memory commands).
@@ -93,6 +105,11 @@ module Insika
93
105
  # when no pack declares harvest) so the engine, the Studio and the
94
106
  # LGPD purges always have a store.
95
107
  harvest_store = Insika::HarvestStore.new(store: backend)
108
+ # the report destination — one record per run, no
109
+ # versioning. Built unconditionally (empty and free when nothing saves)
110
+ # so the tool, the routes, the retention sweep and the tenant purge
111
+ # always have a store.
112
+ artifact_store = Insika::ArtifactStore.new(store: backend)
96
113
 
97
114
  code_tool_registry = Insika::ToolRegistry.new
98
115
  workflow_registry = Insika::WorkflowRegistry.new
@@ -118,7 +135,10 @@ module Insika
118
135
  funnel_store: funnel_store,
119
136
  contact_store: contact_store,
120
137
  followup_store: followup_store,
138
+ schedule_store: schedule_store,
121
139
  proposal_store: proposal_store,
140
+ knowledge_store: knowledge_store,
141
+ artifact_store: artifact_store,
122
142
  code_tool_registry: code_tool_registry,
123
143
  workflow_registry: workflow_registry, policy_registry: policy_registry,
124
144
  capability_registry: Insika::CapabilityRegistry.new, hooks: Insika::Hooks.new,
@@ -141,6 +161,13 @@ module Insika
141
161
  def build(spine:, profiles:, tool_registry:, tool_catalog:, skill_catalog:,
142
162
  prompt_catalog:, guardrails:, context_providers:, edge_limiter: nil,
143
163
  executor_extra: {})
164
+ # the save_artifact tool: a REGISTRY tool registered OPTIONAL (the
165
+ # per-agent allowlist is the switch — an agent that did not name it
166
+ # cannot call it). Registered in the shared code registry, so both
167
+ # roots (base registry + the deployment's overlay, which composes it)
168
+ # expose it. The gem's require lives IN the factory block (loaded on
169
+ # the 1st instance, turn time -> wiring-load stays gem-free).
170
+ register_artifact_tool(spine)
144
171
  spine.hooks.register(:task, after: guardrails.output_validator)
145
172
  middleware = Insika::MiddlewareStack.new([edge_limiter, guardrails.input_guardrail].compact)
146
173
 
@@ -188,6 +215,9 @@ module Insika
188
215
  # parity). The base graph wires it over the same backend the context
189
216
  # trace uses.
190
217
  model_visible_trace_store: Insika::ModelVisibleTraceStore.new(store: spine.backend),
218
+ # the per-turn extraction hook (nil = the loop is off,
219
+ # parity). Gated per-agent by `profile.knowledge`.
220
+ knowledge_store: spine.knowledge_store,
191
221
  **executor_extra
192
222
  )
193
223
 
@@ -233,7 +263,8 @@ module Insika
233
263
  followup_store: spine.followup_store, # records age out too
234
264
  contact_store: spine.contact_store, # cells age out too
235
265
  proposal_store: spine.proposal_store, # proposals age out too
236
- harvest_store: spine.harvest_store # candidates/log/snapshots too
266
+ harvest_store: spine.harvest_store, # candidates/log/snapshots too
267
+ artifact_store: spine.artifact_store # reports expire on their own TTL
237
268
  ),
238
269
  interval: tick_env("INSIKA_TICK_INTERVAL") || Insika::Tick::DEFAULT_INTERVAL,
239
270
  stale_after: tick_env("INSIKA_TICK_STALE_AFTER") || Insika::Tick::DEFAULT_STALE_AFTER
@@ -256,6 +287,16 @@ module Insika
256
287
  contact_store: spine.contact_store, task_store: spine.task_store,
257
288
  profiles: profiles, executor: executor, event_stream: spine.event_stream
258
289
  )
290
+ # the recurring-schedule firer — the tick's
291
+ # fourth duty, wired here after the Tick, gated by its own claim
292
+ # window. Always built: a pack that declares `schedules` later finds
293
+ # its firer already on the tick (inert when no profile declares one).
294
+ executor.tick.schedule = Insika::ScheduleEngine.new(
295
+ store: spine.backend, schedule_store: spine.schedule_store,
296
+ task_store: spine.task_store, session_store: spine.session_store,
297
+ profiles: profiles, executor: executor, budget_ledger: spine.budget_ledger,
298
+ event_stream: spine.event_stream
299
+ )
259
300
  # the distillation engine — the tick-duty that finds idle
260
301
  # customer sessions and distills them on its own worker fiber (a
261
302
  # supervisor child, started in serving mode next to the tick).
@@ -307,13 +348,16 @@ module Insika
307
348
  memory_store: spine.memory_store, memory_audit_store: spine.memory_audit_store,
308
349
  refinement_store: spine.refinement_store,
309
350
  harvest_store: spine.harvest_store,
351
+ artifact_store: spine.artifact_store,
310
352
  outbox_store: spine.outbox_store, shadow_pair_store: spine.shadow_pair_store,
311
353
  inbound_log: spine.inbound_log,
312
354
  outcome_store: spine.outcome_store,
313
355
  funnel_store: spine.funnel_store,
314
356
  contact_store: spine.contact_store,
315
357
  followup_store: spine.followup_store,
358
+ schedule_store: spine.schedule_store,
316
359
  proposal_store: spine.proposal_store,
360
+ knowledge_store: spine.knowledge_store,
317
361
  token_store: spine.token_store, budget_ledger: spine.budget_ledger,
318
362
  circuit_state: spine.circuit_state,
319
363
  channel_registry: spine.channel_registry, channel_delivery: channel_delivery,
@@ -327,6 +371,49 @@ module Insika
327
371
  )
328
372
  end
329
373
 
374
+ # The boot step behind every root's `load_plugins` (Server::Boot's first
375
+ # named step). Discovers plugin manifests, validates them, requires the
376
+ # entries and registers their contributions into the ALREADY-BUILT graph —
377
+ # which is safe by construction: every seam the Loader touches resolves at
378
+ # turn time (registries and hooks by name, the middleware stack and the
379
+ # provider list per call, the catalogs on reload), and the step runs
380
+ # single-fiber before the server accepts connections.
381
+ #
382
+ # Roots, highest precedence first (duplicate id -> first root wins):
383
+ # 1. workspace — INSIKA_PLUGIN_DIR (the operator's override spot);
384
+ # 2. gems — whatever called Insika::Plugin.announce (default-enabled);
385
+ # 3. bundled — `bundled_root` (the repo's plugins/; requires enabling).
386
+ # INSIKA_PLUGINS enables workspace/bundled ids; INSIKA_PLUGINS_DISABLED is
387
+ # the absolute veto (deny wins, like every allowlist in the engine).
388
+ def load_plugins(graph, env: ENV, bundled_root: nil)
389
+ announced = Insika::Plugin.announced_roots
390
+ workspace = Insika::EnvSchema.read("INSIKA_PLUGIN_DIR", env)
391
+ roots = ([workspace] + announced + [bundled_root]).compact.map { |d| File.expand_path(d.to_s) }
392
+
393
+ result = Insika::Plugin::Loader.new(
394
+ roots: roots,
395
+ registries: {
396
+ tools: graph.code_tool_registry, workflows: graph.workflow_registry,
397
+ policies: graph.policy_registry, capabilities: graph.capability_registry,
398
+ channels: graph.channel_registry, hooks: graph.hooks,
399
+ middleware: graph.middleware, context_providers: graph.context_providers
400
+ },
401
+ enabled: plugin_csv(Insika::EnvSchema.read("INSIKA_PLUGINS", env)),
402
+ disabled: plugin_csv(Insika::EnvSchema.read("INSIKA_PLUGINS_DISABLED", env)),
403
+ announced_roots: announced, event_stream: graph.event_stream
404
+ ).load_all
405
+
406
+ # Plugin knowledge joins at the LOWEST precedence: an operator's
407
+ # workspace/authored skill always beats a same-named plugin one.
408
+ graph.skill_catalog.add_roots(result[:skill_dirs])
409
+ graph.prompt_catalog.add_roots(result[:prompt_dirs])
410
+ result
411
+ end
412
+
413
+ def plugin_csv(value)
414
+ value.to_s.split(",").map(&:strip).reject(&:empty?)
415
+ end
416
+
330
417
  # The CORE command surface every root needs — turn essentials + the operator
331
418
  # controls (pause/approve) the Studio dispatches. Registering pause_task/
332
419
  # approve_action HERE is the crux of: it retires the config.ru:28-34 patch.
@@ -411,16 +498,18 @@ module Insika
411
498
  model_visible_trace_store: Insika::ModelVisibleTraceStore.new(store: spine.backend),
412
499
  outcome_store: spine.outcome_store,
413
500
  funnel_store: spine.funnel_store, # the fold dies with the tenant
414
- followup_store: spine.followup_store,
415
- contact_store: spine.contact_store,
416
- proposal_store: spine.proposal_store,
417
- harvest_store: spine.harvest_store,
418
- task_store: spine.task_store, checkpoint_store: spine.checkpoint_store,
419
- outbox_store: spine.outbox_store,
420
- shadow_pairs: spine.shadow_pair_store,
421
- token_store: spine.token_store, # revoked BEFORE the sweep
422
- event_stream: spine.event_stream
423
- ))
501
+ followup_store: spine.followup_store,
502
+ contact_store: spine.contact_store,
503
+ proposal_store: spine.proposal_store,
504
+ harvest_store: spine.harvest_store,
505
+ schedule_store: spine.schedule_store,
506
+ artifact_store: spine.artifact_store,
507
+ task_store: spine.task_store, checkpoint_store: spine.checkpoint_store,
508
+ outbox_store: spine.outbox_store,
509
+ shadow_pairs: spine.shadow_pair_store,
510
+ token_store: spine.token_store, # revoked BEFORE the sweep
511
+ event_stream: spine.event_stream
512
+ ))
424
513
  # the operator's baseline freeze — the number
425
514
  # read. Synchronous control command, dispatched from the Studio.
426
515
  bus.register(:freeze_funnel_baseline,
@@ -434,6 +523,12 @@ module Insika
434
523
  bus.register(:cancel_followup,
435
524
  Insika::Commands::CancelFollowup.new(followup_store: spine.followup_store,
436
525
  event_stream: spine.event_stream))
526
+ # the report destination's Studio mutation — delete one
527
+ # artifact (the only write on the Artifacts tab; a bus command, like
528
+ # every Studio mutation).
529
+ bus.register(:delete_artifact,
530
+ Insika::Commands::DeleteArtifact.new(artifact_store: spine.artifact_store,
531
+ event_stream: spine.event_stream))
437
532
  bus.register(:revoke_contact,
438
533
  Insika::Commands::RevokeContact.new(contact_store: spine.contact_store,
439
534
  followup_store: spine.followup_store,
@@ -493,6 +588,23 @@ module Insika
493
588
  # code path; the engine keeps the automatic pass when the root does
494
589
  # not pass a runner here (nil = the command is not on the bus).
495
590
  bus.register(:run_distillation, run_distillation) if run_distillation
591
+ # the demo seed's ONE path — the CLI's `insika demo:seed` builds
592
+ # the same Insika::Demo::Seeder itself (no executor, no bus, from
593
+ # Graph.spine alone), and the Studio's "Seed demo data" button
594
+ # dispatches this bus command; nothing else writes the demo agent.
595
+ config_store = Insika::ConfigStore.new(store: spine.backend)
596
+ demo_seeder = Insika::Demo::Seeder.new(
597
+ profiles: profiles, store: spine.backend, session_store: spine.session_store,
598
+ task_store: spine.task_store, outcome_store: spine.outcome_store,
599
+ funnel_store: spine.funnel_store, followup_store: spine.followup_store,
600
+ refinement_store: spine.refinement_store, pending_action_store: spine.pending_action_store,
601
+ proposal_store: spine.proposal_store, memory_store: spine.memory_store,
602
+ golden_store: Insika::GoldenStore.new(config_store: config_store),
603
+ baseline_store: Insika::BaselineStore.new(config_store: config_store),
604
+ event_stream: spine.event_stream
605
+ )
606
+ bus.register(:seed_demo_data,
607
+ Insika::Commands::SeedDemoData.new(seeder: demo_seeder, event_stream: spine.event_stream))
496
608
  bus
497
609
  end
498
610
 
@@ -505,7 +617,9 @@ module Insika
505
617
  :harvest_store,
506
618
  :token_store, :budget_ledger, :circuit_state, :outbox_store, :shadow_pair_store,
507
619
  :inbound_log, :outcome_store, :funnel_store,
508
- :contact_store, :followup_store, :proposal_store,
620
+ :contact_store, :followup_store, :schedule_store, :proposal_store,
621
+ :knowledge_store,
622
+ :artifact_store,
509
623
  :code_tool_registry, :workflow_registry, :policy_registry, :capability_registry, :hooks,
510
624
  :channel_registry, keyword_init: true
511
625
  )
@@ -520,7 +634,10 @@ module Insika
520
634
  :inbound_log, :token_store,
521
635
  :budget_ledger, :circuit_state, :outcome_store, :funnel_store,
522
636
  :contact_store, :followup_store, :proposal_store,
637
+ :schedule_store,
523
638
  :harvest_store,
639
+ :knowledge_store,
640
+ :artifact_store,
524
641
  :channel_registry, :channel_delivery,
525
642
  :code_tool_registry, :tool_registry, :workflow_registry, :policy_registry, :capability_registry,
526
643
  :tool_catalog, :skill_catalog, :prompt_catalog,
@@ -531,6 +648,61 @@ module Insika
531
648
  ) do
532
649
  def durable? = backend.is_a?(Insika::Stores::SQLite)
533
650
  end
651
+
652
+ # The save_artifact tool, registered OPTIONAL on the shared code registry
653
+ # (the per-agent allowlist is the switch). The signing/base config is
654
+ # bound at registration time from the environment — the tool instance is
655
+ # per-deployment, and the tenant/agent/task bindings arrive per-turn via
656
+ # the deposited turn context.
657
+ # `run_persona_eval` (C3.1): a QA agent's own probe against a SIBLING
658
+ # agent of THIS SAME graph. Registered OPTIONAL on the shared code
659
+ # registry, same switch as `save_artifact` — but unlike that one, this
660
+ # is NOT called unconditionally from `build`: it needs a `golden_store`
661
+ # and a `settings_store` (the persona model + judge panel), and neither
662
+ # lives on the spine (the base/minimal wiring builds without either, the
663
+ # same way it builds without a Studio) — so each root calls this itself,
664
+ # once it has both. `config/deployment.rb` (round1, and the actual
665
+ # production `config.ru` root) and `DSL::Runtime` both do.
666
+ #
667
+ # `graph` is the ALREADY-RETURNED `Graph::Result` — bus/executor fully
668
+ # assembled by the time a caller has it to pass here, so `GraphChat.new`
669
+ # needs no laziness of its own (the tool's own factory block is what is
670
+ # lazy — this method just registers it).
671
+ # `llm:` — the caller's own RubyLLM::Context, when it has one
672
+ # (DSL::Runtime does; config/deployment.rb does not, and reads the
673
+ # process-wide RubyLLM constant like the rest of that root already
674
+ # does). Forwarded as-is so the tool's OWN persona/judge calls spend
675
+ # the right credentials instead of silently defaulting to nil.
676
+ def register_persona_eval_tool(graph, golden_store:, settings_store:, llm: nil)
677
+ graph.code_tool_registry.register("run_persona_eval", optional: true) do
678
+ require "ruby_llm"
679
+ require_relative "../tools/run_persona_eval"
680
+ Insika::Tools::RunPersonaEval.new(
681
+ golden_store: golden_store, profiles: graph.profiles, tool_registry: graph.tool_registry,
682
+ runtime: GraphChat.new(graph: graph), graph: graph, settings_store: settings_store,
683
+ budget_ledger: graph.budget_ledger, event_stream: graph.event_stream, llm: llm
684
+ )
685
+ end
686
+ end
687
+
688
+ def register_artifact_tool(spine)
689
+ signing_key = Insika::EnvSchema.read("INSIKA_ARTIFACT_SIGNING_KEY")
690
+ signing_ttl = Insika::EnvSchema.read("INSIKA_ARTIFACT_SIGNING_TTL")
691
+ max_bytes = Insika::EnvSchema.read("INSIKA_ARTIFACT_MAX_BYTES")
692
+ base_url = Insika::Coercion.presence(Insika::EnvSchema.read("INSIKA_PUBLIC_URL"))
693
+ spine.code_tool_registry.register("save_artifact", optional: true) do
694
+ require "ruby_llm"
695
+ require_relative "../tools/save_artifact"
696
+ Insika::Tools::SaveArtifact.new(
697
+ artifact_store: spine.artifact_store,
698
+ base_url: base_url,
699
+ signing_key: signing_key,
700
+ signing_ttl: signing_ttl&.to_i,
701
+ max_bytes: max_bytes&.to_i,
702
+ event_stream: spine.event_stream
703
+ )
704
+ end
705
+ end
534
706
  end
535
707
  end
536
708
  end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Insika
4
+ module Wiring
5
+ # A synchronous, in-process "chat" seam over an assembled `Graph::Result`:
6
+ # dispatch a Command, drain the event stream to its terminal event, hand
7
+ # back the outcome. Extracted from `DSL::Runtime#chat`/`#run_workflow`
8
+ # (C3.1) so a graph built OUTSIDE the DSL — `config/deployment.rb`, the
9
+ # actual production/round1 composition root `config.ru` boots — can hand a
10
+ # `#chat`-capable object to anything that needs one (today:
11
+ # `run_persona_eval`'s `Evals::GraphTransport` seam), without a second copy
12
+ # of this dispatch+drain plumbing. `DSL::Runtime` now delegates here too —
13
+ # one implementation, exercised by both roots.
14
+ class GraphChat
15
+ TERMINAL = %i[task_completed task_failed task_cancelled].freeze
16
+
17
+ def initialize(graph:)
18
+ @graph = graph
19
+ end
20
+
21
+ # One turn, in-process -> the assistant's text. `agent:` is REQUIRED —
22
+ # this seam has no notion of "the default agent" (that is a DSL::Runtime
23
+ # concept, filled in by its own caller before delegating here). Raises
24
+ # Insika::Error on a failed/cancelled turn, or the command's own
25
+ # rejection (unknown agent, bad schema) verbatim.
26
+ def chat(message, agent:, session_id: nil, timeout: nil)
27
+ command = Insika::Command.build(
28
+ :send_message, { agent: agent.to_s, message: message, session_id: session_id },
29
+ transport: :cli
30
+ )
31
+ run_command(command, session_id: session_id, timeout: timeout)[:text]
32
+ end
33
+
34
+ # Dispatch + drain, generic over any Command — the seam a workflow run
35
+ # also needs (a :trigger_workflow Command carries :output, not :text).
36
+ # Subscribes BEFORE dispatching (the fiber may emit eagerly) and binds to
37
+ # the dispatched task. A synchronous handler error (unknown agent/
38
+ # workflow, bad input against the schema) propagates untouched — the
39
+ # caller sees the real ValidationError/NotFoundError, not a turn failure.
40
+ def run_command(command, session_id:, timeout:)
41
+ require "async"
42
+ outcome = {}
43
+ rejected = nil
44
+ Async do |task|
45
+ serving = !session_id.nil?
46
+ @graph.executor.supervised = true if serving
47
+ ensure_session(session_id) if session_id
48
+ sub = @graph.event_stream.subscribe
49
+ res = begin
50
+ @graph.bus.dispatch(command)
51
+ rescue StandardError => e
52
+ # CAPTURED, not re-raised inside the reactor: letting it escape the
53
+ # Async block logs "Task may have ended with unhandled exception" —
54
+ # alarming noise for a DOCUMENTED rejection (a schema violation, an
55
+ # unknown agent). Re-raised verbatim below, outside the reactor.
56
+ sub.close
57
+ rejected = e
58
+ next
59
+ end
60
+ sub.bind(task_id: res[:task_id])
61
+ drain(sub, outcome, task, timeout)
62
+ teardown_serving if serving
63
+ end.wait
64
+ raise rejected if rejected
65
+ raise Insika::Error, "turn #{outcome[:error]}" if outcome[:error]
66
+
67
+ outcome
68
+ end
69
+
70
+ private
71
+
72
+ # Reads the stream until this turn reaches a terminal event, then stops.
73
+ # A workflow run also carries its typed OUTPUT, which is not the turn text.
74
+ def drain(sub, outcome, task, timeout)
75
+ reader = task.async do
76
+ sub.each do |ev|
77
+ case ev.type
78
+ when :task_completed then outcome[:text] = ev.data[:content].to_s
79
+ when :workflow_completed then outcome[:output] = ev.data[:output]
80
+ when :task_failed then outcome[:error] = "failed: #{ev.data[:message] || ev.data[:error]}"
81
+ when :task_cancelled then outcome[:error] = "cancelled"
82
+ end
83
+ sub.close if TERMINAL.include?(ev.type)
84
+ end
85
+ end
86
+ timeout ? task.with_timeout(timeout) { reader.wait } : reader.wait
87
+ rescue Async::TimeoutError
88
+ outcome[:error] = "timed out after #{timeout}s"
89
+ sub.close
90
+ end
91
+
92
+ def ensure_session(session_id)
93
+ @graph.session_store.find(session_id) || @graph.session_store.create(id: session_id, vars: {})
94
+ end
95
+
96
+ def teardown_serving
97
+ @graph.executor.stop_session_actors
98
+ @graph.executor.instance_variable_get(:@supervisor)&.stop
99
+ end
100
+ end
101
+ end
102
+ end
data/lib/insika.rb CHANGED
@@ -1,5 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Templates under lib/insika/templates/*/agent.rb open with a bare
4
+ # `require "insika"` (the line a standalone `ruby agent.rb` run needs) and
5
+ # Templates.evaluate instance_evals that same source in-process — so this
6
+ # file's own directory must be on $LOAD_PATH, or that require raises
7
+ # LoadError whenever nothing else already activated the "insika" gem
8
+ # (no Bundler.setup, no globally installed gem).
9
+ $LOAD_PATH.unshift(__dir__) unless $LOAD_PATH.include?(__dir__)
10
+
3
11
  require_relative "insika/version"
4
12
  require_relative "insika/errors"
5
13
  # the payload selection + the domain-boundary audit yardstick.
@@ -27,6 +35,20 @@ require_relative "insika/contact_store"
27
35
  require_relative "insika/followup_store"
28
36
  # the tick-driven firer — rides the same tick as retention/funnel.
29
37
  require_relative "insika/followup_engine"
38
+ # IANA zone resolution through the OS tz database — the stdlib-only route
39
+ # to a zone NAME (Follow-up quiet hours, the cron scheduler, the doctor).
40
+ require_relative "insika/timezone"
41
+ # the recurring schedule: the five-field cron subset + next-fire
42
+ # materialization, the declaration parse, the per-agent rows, and the
43
+ # tick-driven firer (the recurring half of the tick).
44
+ require_relative "insika/cron"
45
+ require_relative "insika/schedule"
46
+ require_relative "insika/schedule_store"
47
+ require_relative "insika/schedule_engine"
48
+ # the report destination — one record per run, no
49
+ # versioning; the signing half of the sharing link (HMAC, no secret stored).
50
+ require_relative "insika/artifact_store"
51
+ require_relative "insika/artifact_signing"
30
52
  require_relative "insika/commands/freeze_funnel_baseline"
31
53
  require_relative "insika/channels/webhook"
32
54
  require_relative "insika/coercion"
@@ -81,6 +103,7 @@ require_relative "insika/context/providers/skill"
81
103
  require_relative "insika/context/providers/skill_trigger"
82
104
  require_relative "insika/context/providers/tool_search"
83
105
  require_relative "insika/context/providers/memory"
106
+ require_relative "insika/context/providers/knowledge"
84
107
  require_relative "insika/context/providers/session"
85
108
  require_relative "insika/context/providers/briefing"
86
109
  # the cache-prefix hash chain + the per-agent cache-hit series.
@@ -112,6 +135,7 @@ require_relative "insika/outbox_store"
112
135
  require_relative "insika/shadow_pair_store"
113
136
  require_relative "insika/inbound_log"
114
137
  require_relative "insika/memory_store"
138
+ require_relative "insika/knowledge_store"
115
139
  # the operator-mutation audit trail. After memory_store (no
116
140
  # mutual deps). Pure stdlib — `load_guard_spec` stays green.
117
141
  require_relative "insika/memory_audit_store"
@@ -122,6 +146,7 @@ require_relative "insika/skill_store"
122
146
  require_relative "insika/secret_masking"
123
147
  require_relative "insika/tool_store"
124
148
  require_relative "insika/tool_trace_store"
149
+ require_relative "insika/tool_usage_report"
125
150
  require_relative "insika/context_trace_store"
126
151
  # the model-visible payload of one ask + its durable trace —
127
152
  # "model-visible means logged" (the conformance suite's reconstruction half).
@@ -172,6 +197,10 @@ require_relative "insika/evals/runner"
172
197
  require_relative "insika/evals/report"
173
198
  require_relative "insika/evals/baseline"
174
199
  require_relative "insika/evals/transport"
200
+ # simulated users: the persona data + the conversation driver.
201
+ # Pure — the persona model is an injected ask, the target rides the Transport.
202
+ require_relative "insika/evals/persona"
203
+ require_relative "insika/evals/simulator"
175
204
  # shadow parity: the frozen criterion and the mechanical
176
205
  # fold over the pair store (C6). Pure — no store, no provider gem.
177
206
  require_relative "insika/parity/criterion"
@@ -189,6 +218,14 @@ require_relative "insika/refinement/panel"
189
218
  require_relative "insika/mcp_store"
190
219
  require_relative "insika/mcp_http_client"
191
220
  require_relative "insika/mcp_tool_ingestor"
221
+ # require "ruby_llm/mcp" itself is LAZY, inside .for — this require_relative only
222
+ # loads the thin factory (see spec/insika/load_guard_spec.rb).
223
+ require_relative "insika/mcp_client"
224
+ # insika/mcp_live_tool (< RubyLLM::Tool) is NOT required here — McpToolRegistry
225
+ # require_relative's it lazily on the 1st instance (turn time), same as
226
+ # OverlayToolRegistry does for tools/data_defined_tool.
227
+ require_relative "insika/mcp_tool_registry"
228
+ require_relative "insika/mcp_json"
192
229
  require_relative "insika/system_file_store"
193
230
  require_relative "insika/recovery"
194
231
  require_relative "insika/command_bus"
@@ -236,6 +273,13 @@ require_relative "insika/commands/export_customer_memory"
236
273
  # record; revoke a contact + fall its pending records atomically).
237
274
  require_relative "insika/commands/cancel_followup"
238
275
  require_relative "insika/commands/revoke_contact"
276
+ # the report destination's Studio mutation — delete one artifact
277
+ # (a bus command, like every Studio mutation).
278
+ require_relative "insika/commands/delete_artifact"
279
+ # in-session compaction (RFC-0044) — the summarizer (pure, injected
280
+ # ask) and the boundary math; the trigger lives in the Executor, the
281
+ # persistence in SessionStore, the read path in the Session provider.
282
+ require_relative "insika/compaction"
239
283
  # session distillation — the distiller (pure, injected ask)
240
284
  # and the proposal store (proposals + the latched dedup ledger + the
241
285
  # per-session distilled marker).
@@ -259,6 +303,15 @@ require_relative "insika/commands/promote_harvest"
259
303
  require_relative "insika/commands/rollback_harvest"
260
304
  require_relative "insika/commands/reject_harvest"
261
305
  require_relative "insika/harvest_engine"
306
+ # post-turn learning — the extractor (pure, injected ask) and the
307
+ # concept format itself (markdown + YAML frontmatter, the SkillCatalog
308
+ # split). The provider touch stays inside ExtractorFactory's lazy ask
309
+ # (load_guard stays green).
310
+ require_relative "insika/knowledge"
311
+ require_relative "insika/commands/backfill_knowledge"
312
+ require_relative "insika/commands/write_concept"
313
+ require_relative "insika/commands/delete_concept"
314
+ require_relative "insika/commands/restore_concept"
262
315
  require_relative "insika/commands/upsert_mcp"
263
316
  require_relative "insika/commands/delete_mcp"
264
317
  require_relative "insika/commands/write_system_file"
@@ -269,6 +322,14 @@ require_relative "insika/commands/delete_data_tool"
269
322
  require_relative "insika/commands/restore_data_tool"
270
323
  require_relative "insika/commands/import_tools"
271
324
  require_relative "insika/commands/import_mcp_tools"
325
+ require_relative "insika/commands/refresh_mcp_tools"
326
+ # the demo seed (OSS onboarding): one bundled agent + golden
327
+ # set + the Seeder that populates every loop's stores at once, and its bus
328
+ # adapter for `insika demo:seed` and the Studio's "Seed demo data" button.
329
+ require_relative "insika/demo/agent_attrs"
330
+ require_relative "insika/demo/golden_cases"
331
+ require_relative "insika/demo/seeder"
332
+ require_relative "insika/commands/seed_demo_data"
272
333
  require_relative "insika/event_stream"
273
334
  require_relative "insika/task_actor"
274
335
  require_relative "insika/queue_policy"
@@ -276,7 +337,9 @@ require_relative "insika/session_actor"
276
337
  require_relative "insika/skill_catalog"
277
338
  require_relative "insika/tool_catalog"
278
339
  require_relative "insika/steer_injector"
340
+ require_relative "insika/tool_batch"
279
341
  require_relative "insika/loop_detector"
342
+ require_relative "insika/turn_budget"
280
343
  require_relative "insika/balloon_splitter"
281
344
  require_relative "insika/turn_output"
282
345
  require_relative "insika/turn_state"
@@ -290,6 +353,7 @@ require_relative "insika/shutdown"
290
353
  require_relative "insika/tick"
291
354
  require_relative "insika/pack"
292
355
  require_relative "insika/pack_importer"
356
+ require_relative "insika/templates"
293
357
  require_relative "insika/telemetry"
294
358
  # Strict config + diagnosis. Doctor reads the config stores above;
295
359
  # EnvSchema (required at the top) is its env layer.