insika 0.8.0 → 1.0.1

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 (276) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +312 -2
  3. data/README.md +10 -6
  4. data/bin/insika +4 -1
  5. data/bin/insika-router +0 -1
  6. data/docs/AGENTS.md +142 -18
  7. data/docs/API.md +73 -0
  8. data/docs/ARCHITECTURE.md +56 -50
  9. data/docs/ARTIFACTS.md +33 -3
  10. data/docs/BENCHMARK.md +50 -7
  11. data/docs/CHANNELS.md +19 -2
  12. data/docs/CONTEXT.md +96 -27
  13. data/docs/CROSS-HARNESS-BENCH.md +157 -0
  14. data/docs/DEPLOY.md +70 -13
  15. data/docs/EMBEDDING.md +10 -0
  16. data/docs/EVALS.md +129 -10
  17. data/docs/FACTS.md +12 -6
  18. data/docs/HARVEST.md +8 -3
  19. data/docs/KNOWLEDGE.md +14 -0
  20. data/docs/MEDIA.md +6 -2
  21. data/docs/OBSERVABILITY.md +164 -28
  22. data/docs/PERFORMANCE.md +110 -0
  23. data/docs/PLUGINS.md +4 -4
  24. data/docs/POLICY.md +26 -14
  25. data/docs/RELEASING.md +9 -0
  26. data/docs/ROUTER.md +7 -0
  27. data/docs/RUBYLLM_2_MIGRATION.md +307 -0
  28. data/docs/SECURITY.md +34 -1
  29. data/docs/SHARED-CONVERSATIONS.md +40 -0
  30. data/docs/SKILLS.md +5 -3
  31. data/docs/STUDIO.md +122 -0
  32. data/docs/TOOLS.md +238 -42
  33. data/docs/operate.md +3 -0
  34. data/docs/prompts/ADD-TOOL.md +12 -2
  35. data/docs/prompts/DIAGNOSE-TURN.md +3 -0
  36. data/docs/prompts/GO-LIVE.md +3 -1
  37. data/lib/insika/agent_defaults.rb +140 -0
  38. data/lib/insika/agent_file_store.rb +12 -0
  39. data/lib/insika/agent_profile.rb +99 -17
  40. data/lib/insika/artifact_signing.rb +11 -0
  41. data/lib/insika/channels/web/widget.js +33 -0
  42. data/lib/insika/channels/web.rb +5 -2
  43. data/lib/insika/chat_builder.rb +124 -28
  44. data/lib/insika/checkpoint.rb +6 -1
  45. data/lib/insika/checkpoint_store.rb +59 -7
  46. data/lib/insika/commands/agent_payload.rb +3 -3
  47. data/lib/insika/commands/delete_kit.rb +26 -0
  48. data/lib/insika/commands/gate_harvest.rb +3 -0
  49. data/lib/insika/commands/review_guardrail_flag.rb +38 -0
  50. data/lib/insika/commands/run_distillation.rb +5 -8
  51. data/lib/insika/commands/run_harvest.rb +17 -1
  52. data/lib/insika/commands/seed_session.rb +128 -0
  53. data/lib/insika/commands/send_message.rb +9 -3
  54. data/lib/insika/commands/update_settings.rb +23 -1
  55. data/lib/insika/commands/write_kit.rb +27 -0
  56. data/lib/insika/compaction.rb +33 -7
  57. data/lib/insika/config_store.rb +87 -7
  58. data/lib/insika/context/builder.rb +29 -9
  59. data/lib/insika/context/priority.rb +5 -0
  60. data/lib/insika/context/provider.rb +7 -2
  61. data/lib/insika/context/providers/briefing.rb +11 -8
  62. data/lib/insika/context/providers/fence_notice.rb +27 -0
  63. data/lib/insika/context/providers/knowledge.rb +32 -6
  64. data/lib/insika/context/providers/memory.rb +61 -8
  65. data/lib/insika/context/providers/pending_confirmation.rb +52 -0
  66. data/lib/insika/context/providers/prompt.rb +17 -4
  67. data/lib/insika/context/providers/session.rb +46 -6
  68. data/lib/insika/context/reranker.rb +75 -0
  69. data/lib/insika/distill.rb +22 -13
  70. data/lib/insika/doctor.rb +142 -4
  71. data/lib/insika/dsl/runtime.rb +37 -4
  72. data/lib/insika/dsl/server_boot.rb +4 -12
  73. data/lib/insika/dsl.rb +19 -5
  74. data/lib/insika/edge_limiter.rb +27 -16
  75. data/lib/insika/egress_guard.rb +11 -3
  76. data/lib/insika/env_schema.rb +33 -73
  77. data/lib/insika/errors.rb +1 -0
  78. data/lib/insika/evals/assertions.rb +259 -18
  79. data/lib/insika/evals/golden.rb +230 -4
  80. data/lib/insika/evals/judge.rb +19 -10
  81. data/lib/insika/evals/moderator_compare.rb +47 -0
  82. data/lib/insika/evals/report.rb +61 -9
  83. data/lib/insika/evals/runner.rb +64 -4
  84. data/lib/insika/evals/simulator.rb +21 -3
  85. data/lib/insika/evals/transport.rb +317 -15
  86. data/lib/insika/event_stream.rb +63 -7
  87. data/lib/insika/evidence.rb +105 -17
  88. data/lib/insika/executor.rb +698 -164
  89. data/lib/insika/fence.rb +96 -0
  90. data/lib/insika/golden_store.rb +3 -0
  91. data/lib/insika/harvest.rb +26 -14
  92. data/lib/insika/harvest_engine.rb +1 -0
  93. data/lib/insika/harvest_store.rb +12 -0
  94. data/lib/insika/http_client.rb +87 -4
  95. data/lib/insika/kits.rb +54 -0
  96. data/lib/insika/knowledge.rb +34 -20
  97. data/lib/insika/llm_configurator.rb +46 -4
  98. data/lib/insika/llm_provider_store.rb +1 -3
  99. data/lib/insika/llm_trace_store.rb +54 -0
  100. data/lib/insika/mcp_client.rb +27 -63
  101. data/lib/insika/mcp_live_tool.rb +55 -9
  102. data/lib/insika/mcp_store.rb +37 -3
  103. data/lib/insika/mcp_tool_registry.rb +76 -26
  104. data/lib/insika/media.rb +38 -25
  105. data/lib/insika/memory_store.rb +16 -3
  106. data/lib/insika/memory_trim.rb +60 -0
  107. data/lib/insika/middleware.rb +12 -3
  108. data/lib/insika/model_metrics_store.rb +285 -0
  109. data/lib/insika/model_registry.rb +110 -0
  110. data/lib/insika/model_resolver.rb +1 -1
  111. data/lib/insika/model_selection.rb +14 -21
  112. data/lib/insika/model_visible.rb +17 -6
  113. data/lib/insika/onboarding.rb +1 -0
  114. data/lib/insika/outcome_store.rb +1 -1
  115. data/lib/insika/overlay_tool_registry.rb +30 -2
  116. data/lib/insika/pack_importer.rb +54 -9
  117. data/lib/insika/packaging.rb +14 -5
  118. data/lib/insika/pending_action_store.rb +45 -18
  119. data/lib/insika/plugin/loader.rb +4 -2
  120. data/lib/insika/prefix_fingerprint.rb +32 -27
  121. data/lib/insika/profile_source.rb +5 -0
  122. data/lib/insika/provider_error_classifier.rb +18 -3
  123. data/lib/insika/queue_policy.rb +7 -2
  124. data/lib/insika/refinement/proposer.rb +42 -17
  125. data/lib/insika/reliability.rb +12 -3
  126. data/lib/insika/router/app.rb +14 -5
  127. data/lib/insika/router/session_key.rb +35 -5
  128. data/lib/insika/safety/config.rb +20 -3
  129. data/lib/insika/safety/corpus.rb +10 -3
  130. data/lib/insika/safety/factory.rb +39 -1
  131. data/lib/insika/safety/input_guardrail.rb +21 -1
  132. data/lib/insika/safety/judge_moderator.rb +70 -0
  133. data/lib/insika/safety/output_validator.rb +29 -5
  134. data/lib/insika/safety/prompt_echo.rb +89 -0
  135. data/lib/insika/schema_guard.rb +53 -76
  136. data/lib/insika/server/app.rb +58 -7
  137. data/lib/insika/server/rack_app.rb +2 -0
  138. data/lib/insika/server/responses.rb +50 -5
  139. data/lib/insika/session_actor.rb +58 -28
  140. data/lib/insika/session_store.rb +163 -11
  141. data/lib/insika/settings_store.rb +57 -2
  142. data/lib/insika/shared_conversations.rb +305 -0
  143. data/lib/insika/skill_catalog.rb +28 -2
  144. data/lib/insika/skill_store.rb +6 -0
  145. data/lib/insika/soak/runner.rb +1 -1
  146. data/lib/insika/spoken_transcript.rb +31 -0
  147. data/lib/insika/steer_injector.rb +7 -48
  148. data/lib/insika/store.rb +29 -1
  149. data/lib/insika/stores/json_value.rb +37 -0
  150. data/lib/insika/stores/memory.rb +22 -35
  151. data/lib/insika/stores/postgres.rb +295 -0
  152. data/lib/insika/stores/sqlite.rb +257 -43
  153. data/lib/insika/stores/turn_counter.rb +25 -0
  154. data/lib/insika/studio/app.rb +1459 -163
  155. data/lib/insika/studio/assets/dist/application.css +1 -1
  156. data/lib/insika/studio/assets/dist/application.js +25 -25
  157. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  158. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  159. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  160. data/lib/insika/studio/assets/dist/fonts/ibm-plex-sans-latin-wght-normal.woff2 +0 -0
  161. data/lib/insika/studio/forms.rb +189 -12
  162. data/lib/insika/studio/nav_icons.rb +3 -0
  163. data/lib/insika/studio/views/_agent_config_retrieval.erb +73 -0
  164. data/lib/insika/studio/views/_agent_picker.erb +15 -0
  165. data/lib/insika/studio/views/_agent_tab_config.erb +83 -243
  166. data/lib/insika/studio/views/_agent_tab_history.erb +56 -1
  167. data/lib/insika/studio/views/_agent_tab_performance.erb +34 -0
  168. data/lib/insika/studio/views/_agent_tab_prompts.erb +7 -9
  169. data/lib/insika/studio/views/_agent_tab_skills.erb +16 -7
  170. data/lib/insika/studio/views/_agent_tab_tools.erb +90 -0
  171. data/lib/insika/studio/views/_agents_master.erb +8 -12
  172. data/lib/insika/studio/views/_cfg_alerts.erb +4 -0
  173. data/lib/insika/studio/views/_cfg_budget.erb +20 -0
  174. data/lib/insika/studio/views/_cfg_distill.erb +26 -0
  175. data/lib/insika/studio/views/_cfg_followup.erb +30 -0
  176. data/lib/insika/studio/views/_cfg_grounding.erb +21 -0
  177. data/lib/insika/studio/views/_cfg_guardrails.erb +47 -0
  178. data/lib/insika/studio/views/_cfg_harvest.erb +35 -0
  179. data/lib/insika/studio/views/_cfg_reliability.erb +32 -0
  180. data/lib/insika/studio/views/_chats_master.erb +66 -0
  181. data/lib/insika/studio/views/_customers_master.erb +37 -0
  182. data/lib/insika/studio/views/_empty.erb +15 -0
  183. data/lib/insika/studio/views/_facts_master.erb +42 -0
  184. data/lib/insika/studio/views/_harvest_candidate_detail.erb +14 -0
  185. data/lib/insika/studio/views/_identity.erb +16 -0
  186. data/lib/insika/studio/views/_kpi.erb +17 -0
  187. data/lib/insika/studio/views/_message.erb +40 -2
  188. data/lib/insika/studio/views/_model_chart.erb +82 -0
  189. data/lib/insika/studio/views/_refinement_master.erb +38 -0
  190. data/lib/insika/studio/views/{tool_edit.erb → _tool_edit.erb} +38 -11
  191. data/lib/insika/studio/views/_tool_users.erb +17 -0
  192. data/lib/insika/studio/views/_why.erb +53 -0
  193. data/lib/insika/studio/views/agent_detail.erb +17 -10
  194. data/lib/insika/studio/views/agents.erb +28 -16
  195. data/lib/insika/studio/views/approvals.erb +16 -11
  196. data/lib/insika/studio/views/artifact.erb +20 -7
  197. data/lib/insika/studio/views/artifacts.erb +19 -14
  198. data/lib/insika/studio/views/chats.erb +22 -29
  199. data/lib/insika/studio/views/customer.erb +88 -43
  200. data/lib/insika/studio/views/customers.erb +16 -26
  201. data/lib/insika/studio/views/evals.erb +49 -31
  202. data/lib/insika/studio/views/fact_detail.erb +114 -0
  203. data/lib/insika/studio/views/facts.erb +15 -116
  204. data/lib/insika/studio/views/followups.erb +63 -52
  205. data/lib/insika/studio/views/funnel.erb +104 -43
  206. data/lib/insika/studio/views/harvest.erb +85 -59
  207. data/lib/insika/studio/views/home.erb +138 -23
  208. data/lib/insika/studio/views/inbox.erb +36 -0
  209. data/lib/insika/studio/views/kits.erb +176 -0
  210. data/lib/insika/studio/views/knowledge.erb +92 -80
  211. data/lib/insika/studio/views/layout.erb +42 -11
  212. data/lib/insika/studio/views/live.erb +45 -0
  213. data/lib/insika/studio/views/mcp.erb +43 -13
  214. data/lib/insika/studio/views/models.erb +187 -0
  215. data/lib/insika/studio/views/parity.erb +38 -32
  216. data/lib/insika/studio/views/playground.erb +13 -9
  217. data/lib/insika/studio/views/refinement.erb +15 -224
  218. data/lib/insika/studio/views/refinement_detail.erb +243 -0
  219. data/lib/insika/studio/views/session.erb +153 -118
  220. data/lib/insika/studio/views/settings.erb +86 -22
  221. data/lib/insika/studio/views/skills.erb +53 -19
  222. data/lib/insika/studio/views/system_files.erb +7 -7
  223. data/lib/insika/studio/views/task.erb +45 -15
  224. data/lib/insika/studio/views/tasks.erb +35 -18
  225. data/lib/insika/studio/views/tools.erb +62 -73
  226. data/lib/insika/task_actor.rb +4 -1
  227. data/lib/insika/task_store.rb +67 -1
  228. data/lib/insika/telemetry/pricing.rb +16 -14
  229. data/lib/insika/telemetry/recorder.rb +115 -9
  230. data/lib/insika/telemetry/ruby_llm_instrumenter.rb +110 -0
  231. data/lib/insika/telemetry.rb +12 -2
  232. data/lib/insika/templates/daily-digest/README.md +13 -0
  233. data/lib/insika/templates/daily-digest/agent.rb +22 -11
  234. data/lib/insika/testing/store_contract.rb +65 -0
  235. data/lib/insika/text_tool_call.rb +66 -0
  236. data/lib/insika/tick.rb +7 -1
  237. data/lib/insika/token_store.rb +5 -0
  238. data/lib/insika/tool_assembly.rb +21 -13
  239. data/lib/insika/tool_batch.rb +1 -1
  240. data/lib/insika/tool_catalog.rb +12 -2
  241. data/lib/insika/tool_definition.rb +82 -13
  242. data/lib/insika/tool_envelope.rb +266 -29
  243. data/lib/insika/tool_manifest.rb +1 -0
  244. data/lib/insika/tool_store.rb +12 -4
  245. data/lib/insika/tool_trace_store.rb +15 -8
  246. data/lib/insika/tool_usage_report.rb +14 -4
  247. data/lib/insika/tools/a2a_remote.rb +1 -1
  248. data/lib/insika/tools/agent_enum.rb +1 -1
  249. data/lib/insika/tools/confirm_pending.rb +112 -0
  250. data/lib/insika/tools/data_defined_tool.rb +24 -15
  251. data/lib/insika/tools/generate_image.rb +2 -2
  252. data/lib/insika/tools/load_knowledge.rb +7 -3
  253. data/lib/insika/tools/load_skill.rb +3 -3
  254. data/lib/insika/tools/present.rb +115 -0
  255. data/lib/insika/tools/remember.rb +7 -2
  256. data/lib/insika/tools/run_persona_eval.rb +9 -6
  257. data/lib/insika/tools/save_artifact.rb +16 -6
  258. data/lib/insika/tools/schedule_followup.rb +3 -3
  259. data/lib/insika/tools/stuck_signal.rb +3 -3
  260. data/lib/insika/tools/subagent.rb +5 -5
  261. data/lib/insika/tools/subagents.rb +3 -3
  262. data/lib/insika/tools/tool_search.rb +11 -7
  263. data/lib/insika/tools/tts.rb +2 -2
  264. data/lib/insika/tools/update_briefing.rb +4 -4
  265. data/lib/insika/turn_output.rb +2 -2
  266. data/lib/insika/turn_state.rb +31 -1
  267. data/lib/insika/turn_timing.rb +51 -14
  268. data/lib/insika/version.rb +1 -1
  269. data/lib/insika/wiring/graph.rb +88 -16
  270. data/lib/insika/wiring/graph_chat.rb +4 -0
  271. data/lib/insika/workflow.rb +25 -64
  272. data/lib/insika.rb +23 -3
  273. metadata +74 -13
  274. data/lib/insika/commands/import_mcp_tools.rb +0 -48
  275. data/lib/insika/mcp_http_client.rb +0 -67
  276. data/lib/insika/mcp_tool_ingestor.rb +0 -165
@@ -6,13 +6,13 @@ module Insika
6
6
  module Tools
7
7
  # Level 2 of TOOLS progressive disclosure (analog of LoadSkill):
8
8
  # searches the deferred catalog and PROMOTES the relevant ones into the live chat via
9
- # chat.with_tools (verified to propagate on the next round of the same `ask`
10
- # in ruby_llm 1.16). `require "ruby_llm"` stays in THIS file (it inherits from
9
+ # chat.with_tools (propagates on the next round of the same `ask`).
10
+ # `require "ruby_llm"` stays in THIS file (it inherits from
11
11
  # RubyLLM::Tool) — it does not enter lib/insika.rb; the Executor loads it lazily in
12
12
  # configure_chat (like LoadSkill).
13
13
  class ToolSearch < RubyLLM::Tool
14
14
  description "Searches and enables additional tools by describing the need"
15
- param :query, desc: "What you need to do (e.g.: 'send email', 'generate invoice')"
15
+ parameter :query, description: "What you need to do (e.g.: 'send email', 'generate invoice')"
16
16
 
17
17
  # RubyLLM::Tool#name derives from self.class.name — for a nested class
18
18
  # (Insika::Tools::ToolSearch) it produces "insika--tools--tool_search", not
@@ -21,7 +21,7 @@ module Insika
21
21
  def name = "tool_search"
22
22
 
23
23
  def initialize(catalog, deferred_allowed, chat, tool_registry:, event_stream:,
24
- checkpoint_store:, state:)
24
+ checkpoint_store:, state:, trace_recorder: nil)
25
25
  @catalog = catalog
26
26
  @deferred_allowed = Array(deferred_allowed).map(&:to_s)
27
27
  @chat = chat
@@ -29,6 +29,7 @@ module Insika
29
29
  @event_stream = event_stream
30
30
  @checkpoint_store = checkpoint_store
31
31
  @state = state
32
+ @trace_recorder = trace_recorder
32
33
  @promoted = [] # names already promoted IN THIS chat — idempotency
33
34
  super()
34
35
  end
@@ -59,7 +60,8 @@ module Insika
59
60
  @promoted << entry.name
60
61
  ToolEnvelope.new(tool, state: @state, checkpoint_store: @checkpoint_store,
61
62
  tool_registry: @tool_registry, timeout: timeout,
62
- skip_side_effects: Array(@state.skip_side_effects))
63
+ skip_side_effects: Array(@state.skip_side_effects),
64
+ event_stream: @event_stream, trace_recorder: @trace_recorder)
63
65
  rescue Insika::NotFoundError
64
66
  nil
65
67
  end
@@ -79,11 +81,13 @@ module Insika
79
81
 
80
82
  def describe(entry)
81
83
  tool = @tool_registry.resolve(entry.name)
84
+ schema = Coercion.deep_stringify(tool.parameters_schema || {})
82
85
  {
83
86
  name: entry.name,
84
87
  description: entry.description,
85
- parameters: tool.parameters.transform_values do |p|
86
- { type: p.type, description: p.description, required: p.required }
88
+ parameters: schema.fetch("properties", {}).to_h do |name, property|
89
+ [name.to_sym, { type: property["type"], description: property["description"],
90
+ required: Array(schema["required"]).include?(name) }]
87
91
  end
88
92
  }
89
93
  rescue Insika::NotFoundError
@@ -20,8 +20,8 @@ module Insika
20
20
  description "Synthesize speech for the reply — a voice clip travels as an " \
21
21
  "output part in the envelope, the channel supports it. Use when the " \
22
22
  "customer should HEAR the answer rather than read it."
23
- param :text, desc: "The words to speak"
24
- param :voice, desc: "Optional voice override (default from the agent config)",
23
+ parameter :text, description: "The words to speak"
24
+ parameter :voice, description: "Optional voice override (default from the agent config)",
25
25
  required: false
26
26
 
27
27
  def name = "tts"
@@ -53,8 +53,8 @@ module Insika
53
53
  names.empty? ? BASE_DESCRIPTION : "#{BASE_DESCRIPTION} field must be one of: #{names.join(', ')}."
54
54
  end
55
55
 
56
- param :field, desc: "The briefing field name (one of the declared list)"
57
- param :value, desc: "The value learned. Blank clears the field."
56
+ parameter :field, description: "The briefing field name (one of the declared list)"
57
+ parameter :value, description: "The value learned. Blank clears the field."
58
58
 
59
59
  def name = "update_briefing"
60
60
 
@@ -72,7 +72,7 @@ module Insika
72
72
 
73
73
  # The declared names become the `field` enum in the schema the provider
74
74
  # sees — per-turn data, like the subagent allowlist (Tools::AgentEnum).
75
- def params_schema
75
+ def parameters_schema
76
76
  @field_enum_schema ||= Insika::Tools::AgentEnum.inject(super, @fields, path: %i[field])
77
77
  end
78
78
 
@@ -99,7 +99,7 @@ module Insika
99
99
 
100
100
  description "Records the next step agreed with the customer " \
101
101
  "(e.g. 'send the payment link tomorrow at 10'). Blank clears it."
102
- param :text, desc: "The agreed next step, in one sentence"
102
+ parameter :text, description: "The agreed next step, in one sentence"
103
103
 
104
104
  def name = "set_next_step"
105
105
 
@@ -49,12 +49,12 @@ module Insika
49
49
  # public_intermediate: the agent opted this channel in (`edge_stream`), so the
50
50
  # narration is TAGGED and `/v1/responses` gives it its own frame. Default false:
51
51
  # an internal event stays internal unless someone said otherwise.
52
- def initialize(filter:, emit:, public_intermediate: false)
52
+ def initialize(filter:, emit:, public_intermediate: false, halt_text: "")
53
53
  @filter = filter
54
54
  @emit = emit
55
55
  @public_intermediate = public_intermediate
56
56
  @pending = +"" # text of the message currently streaming
57
- @last_intermediate = +"" # text of the last message that turned out NOT to be the answer
57
+ @last_intermediate = halt_text.to_s # already filtered when checkpointed
58
58
  @candidate = nil
59
59
  end
60
60
 
@@ -6,6 +6,10 @@ module Insika
6
6
  class TurnState
7
7
  attr_reader :task, :profile, :turn # turn identity (1-based)
8
8
  attr_accessor :message, # input (Middleware may rewrite)
9
+ :caller_context, # payload `context`: text the caller sends WITH the message
10
+ # (channel rules, what the customer already saw). The model
11
+ # and the transcript get it ahead of the message; guardrails
12
+ # and retrieval read `message` alone. nil = none.
9
13
  :context, # ContextPackage from the Builder
10
14
  :allowed_tools, # Resolution from the Policy Engine
11
15
  :allowed_skills,
@@ -61,9 +65,16 @@ module Insika
61
65
  # context via an evidence-declared tool (the envelope
62
66
  # appends; the validator/enforcer read it). Built per
63
67
  # turn by the Executor; nil = no session/no evidence.
64
- :evidence_attachments, # [ {type, url, caption} ] hoarded by the
68
+ :evidence_attachments, # [ {type, url, caption, id} ] hoarded by the
65
69
  # envelope this turn; read by the Executor at stage 8
66
70
  # for the channel delivery. Reset per turn.
71
+ :presentations, # [ {component, title, items: [{id,url,caption}]} ] —
72
+ # what a presentation tool SELECTED this turn, in call
73
+ # order. Non-empty = the channel delivery sends these
74
+ # instead of every hoarded card. Reset per turn.
75
+ :fence_max_chars, # per-leaf cap the ToolEnvelope applies when the
76
+ # agent has `fencing` on — the platform's
77
+ # `fencing.max_chars`, set per turn by the Executor.
67
78
  :context_trace_entry # the sanitized trace entry parked at
68
79
  # prepare_turn (fingerprints + invalidation_reason);
69
80
  # the stage-8 stamp merges the cache-hit fields into
@@ -104,7 +115,9 @@ module Insika
104
115
  # to serialize the turn's real exchange — user + assistant(tool_calls) + tool
105
116
  # results + final assistant — into the transcript. nil = no chat recorded
106
117
  # (workflow/halt) → persist_turn falls back to the {user, assistant} pair.
118
+ attr_accessor :shared_conversations
107
119
  attr_accessor :chat_baseline
120
+ attr_accessor :tool_halt
108
121
 
109
122
  # Internal: impl_name(String) -> STABLE name of the capability that
110
123
  # resolved it, computed by resolve_capabilities BEFORE the policy_request and
@@ -134,6 +147,9 @@ module Insika
134
147
  # Internal (memory): the turn's tenant (from the Command), scope of the write path
135
148
  # (`remember` tool). Set in run_pipeline; nil = DEFAULT_TENANT in the MemoryStore.
136
149
  attr_accessor :tenant
150
+ # The phrase an injection-flagging InputGuardrail matched (nil = none). The
151
+ # ChatBuilder hands the model a per-turn notice built from it.
152
+ attr_accessor :security_notice
137
153
 
138
154
  # Internal: turn context deposited into the data-tools to
139
155
  # resolve {{ctx.*}} (chat_id/agent_id/tenant/store_id) and emit
@@ -148,6 +164,9 @@ module Insika
148
164
  # (workflow) or provider without counts.
149
165
  attr_accessor :usage
150
166
 
167
+ # Native rerank usage events, separate from the chat/Responses token totals.
168
+ attr_accessor :rerank_usage
169
+
151
170
  # Internal (Tool Search): ids of side-effects already completed in the
152
171
  # interrupted turn, propagated to the tools PROMOTED by tool_search (the same `skip`
153
172
  # that the eager tools' wrap_tools receives). Set in run_pipeline;
@@ -159,6 +178,12 @@ module Insika
159
178
  # creates the PendingAction/suspends/waits; `actor` = the turn's mailbox (used
160
179
  # by the coordinator for await(:approval)).
161
180
  attr_accessor :requires_approval, :approval_coordinator, :actor
181
+ attr_accessor :native_approvals, :approval_continuation, :turn_output
182
+
183
+ # Customer confirmation: the store a `customer_confirm` call is held in by the
184
+ # ToolEnvelope and resolved from by confirm_pending/cancel_pending. nil = the
185
+ # deployment has no PendingActionStore (a hold then fails loud, like approval).
186
+ attr_accessor :pending_action_store
162
187
 
163
188
  # Internal: the turn's shared in-flight cap for tool calls —
164
189
  # ONE Async::Semaphore(tool_concurrency), installed by ToolAssembly#wrap_tools
@@ -167,6 +192,10 @@ module Insika
167
192
  # nil = concurrency off: no gate, no overhead, serial execution unchanged.
168
193
  attr_accessor :tool_gate
169
194
 
195
+ # Turns of one session already run serially through SessionActor. This gate
196
+ # prevents overlapping writes within a parallel batch of the current turn.
197
+ attr_accessor :side_effect_gate
198
+
170
199
  # parallel tool calls, resolved PER TURN and read by ChatBuilder
171
200
  # (whether to hand the gem `concurrency:`) and ToolAssembly (the gate's size).
172
201
  #
@@ -199,6 +228,7 @@ module Insika
199
228
  @output_parts = []
200
229
  @channel_capabilities = []
201
230
  @evidence_attachments = []
231
+ @presentations = []
202
232
  # Fiber storage is INHERITED by fibers created later, so a turn spawned from
203
233
  # inside a tool call (a subagent child) would start out carrying its
204
234
  # parent's correlation. Clearing at turn start keeps a child from keying its
@@ -1,15 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Insika
4
- # Opt-in per-turn latency breakdown (locate the
5
- # TTFB cost with real-turn data). OFF unless INSIKA_TURN_TIMING is set — when
6
- # off the Executor never allocates one and the hot path pays only `nil&.mark`.
4
+ # Per-turn latency clock. Every turn runs one (a few clock reads); the turn's
5
+ # numbers land in ModelMetricsStore's turn rows (#metrics). INSIKA_TURN_TIMING
6
+ # only decides what the RESPONSE exposes (#to_h) and whether store calls count.
7
7
  #
8
8
  # Splits a turn into the three windows that answer "is TTFB local or provider?":
9
9
  # prep_ms — prep_start -> ask: ALL local work before the provider call
10
10
  # (context build, policy, guardrail detectors, chat assembly).
11
11
  # ttft_ms — ask -> first_token: the provider round-trip to the 1st token.
12
12
  # gen_ms — first_token -> done: streaming the rest of the response.
13
+ # store_calls — store calls this turn made (+ store_calls_by: "op scope" => n).
13
14
  #
14
15
  # Marks are monotonic; `mark` is first-write-wins so `first_token` records the
15
16
  # FIRST content chunk even though it is called on every chunk.
@@ -26,26 +27,35 @@ module Insika
26
27
  Insika::EnvSchema.truthy?(Insika::EnvSchema.read("INSIKA_TURN_TIMING", env))
27
28
  end
28
29
 
29
- # The marks that only mean something under INSIKA_TURN_TIMING. A `breakdown:
30
- # false` clock (a channel turn with the flag off) ignores them and measures
31
- # only first_balloon_ms.
32
- BREAKDOWN_MARKS = %i[prep_start ask first_token done].freeze
33
- private_constant :BREAKDOWN_MARKS
30
+ # Fiber-storage slot holding the running turn's clock. Child fibers (parallel
31
+ # tool calls, context providers) inherit it, so their store calls count too.
32
+ FIBER_KEY = :insika_turn_timing
34
33
 
35
- # breakdown: true (the default) is the flag-on clock: prep/ttft/gen/total.
36
- # false is the channel clock: only :inbound -> :first_balloon,
37
- # so a channel turn with the flag off still answers H-latência.
34
+ def self.current = Fiber[FIBER_KEY]
35
+
36
+ # "config:<scope>" keeps its config scope; any other scope keeps its first
37
+ # segment, so ids embedded in scope names do not split the breakdown.
38
+ def self.scope_group(scope)
39
+ parts = scope.to_s.split(":")
40
+ parts.first == "config" ? parts.first(2).join(":") : parts.first.to_s
41
+ end
42
+
43
+ # breakdown: true (the default) exposes prep/ttft/gen/total in #to_h.
44
+ # false exposes only first_balloon_ms (the flag-off contract); every mark is
45
+ # still taken, so #metrics is complete either way.
38
46
  def initialize(breakdown: true)
39
47
  @marks = {}
40
48
  @breakdown = breakdown
49
+ @tools = []
41
50
  end
42
51
 
43
52
  def mark(name)
44
- return unless @breakdown || !BREAKDOWN_MARKS.include?(name)
45
-
46
53
  @marks[name] ||= Process.clock_gettime(Process::CLOCK_MONOTONIC)
47
54
  end
48
55
 
56
+ # One tool call of this turn (ToolEnvelope#trace): [name, ok, ms].
57
+ def tool(name, ok, ms) = @tools << [name.to_s, ok, ms]
58
+
49
59
  # Has this mark fired? Used by `SendMessage` to prove the channel clock was
50
60
  # stamped at 202 acceptance (`:inbound` before the debounce window), and by
51
61
  # the pipeline to know a threaded clock already started.
@@ -56,13 +66,40 @@ module Insika
56
66
  # first_balloon_ms is the inbound -> first outbox flush window.
57
67
  # A missing number is never a zero — no mark, no key.
58
68
  def to_h
59
- {
69
+ h = {
60
70
  prep_ms: delta(:prep_start, :ask),
61
71
  ttft_ms: delta(:ask, :first_token),
62
72
  gen_ms: delta(:first_token, :done),
63
73
  total_ms: delta(:prep_start, :done),
64
74
  first_balloon_ms: delta(:inbound, :first_balloon)
65
75
  }.compact
76
+ h = h.slice(:first_balloon_ms) unless @breakdown
77
+ return h unless @store_calls
78
+
79
+ h.merge(store_calls: @store_calls.values.sum,
80
+ store_calls_by: @store_calls.sort_by { |call, n| [-n, call] }.to_h)
81
+ end
82
+
83
+ # The turn row ModelMetricsStore keeps: every window, whatever the flag.
84
+ # queue_ms is inbound -> pipeline start (debounce + FIFO wait), the part of
85
+ # first_balloon_ms that is not the turn's own work. tools_ms sums the calls,
86
+ # so parallel calls count more than their wall time.
87
+ def metrics
88
+ {
89
+ "prep_ms" => delta(:prep_start, :ask), "ttft_ms" => delta(:ask, :first_token),
90
+ "gen_ms" => delta(:first_token, :done), "total_ms" => delta(:prep_start, :done),
91
+ "first_balloon_ms" => delta(:inbound, :first_balloon), "queue_ms" => delta(:inbound, :prep_start),
92
+ "tools_ms" => (@tools.sum { |_, _, ms| ms.to_i } unless @tools.empty?),
93
+ "tools" => (@tools unless @tools.empty?)
94
+ }.compact
95
+ end
96
+
97
+ # One store call made during this turn (see Stores::TurnCounter).
98
+ def count_store(op, scope)
99
+ return unless @breakdown
100
+
101
+ @store_calls ||= Hash.new(0)
102
+ @store_calls["#{op} #{self.class.scope_group(scope)}"] += 1
66
103
  end
67
104
 
68
105
  private
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Insika
4
- VERSION = "0.8.0"
4
+ VERSION = "1.0.1"
5
5
  end
@@ -24,14 +24,37 @@ module Insika
24
24
 
25
25
  # Backend by config: INSIKA_DB set → durable SQLite (survives restart, the
26
26
  # prerequisite for Recovery); missing → ephemeral Memory (dev/demo). The same
27
- # rule lived verbatim in both roots. Dual-read honors the legacy HARNESS_DB alias.
27
+ # rule lived verbatim in both roots.
28
+ # LITESTREAM_REPLICA_URL is the same switch deploy/entrypoint.sh uses: set, and
29
+ # Litestream owns the checkpoints.
30
+ # Every backend counts its calls into the running turn (TurnTiming).
28
31
  def backend_from_env(env = ENV)
32
+ Insika::Stores::TurnCounter.attach(raw_backend_from_env(env))
33
+ end
34
+
35
+ def raw_backend_from_env(env = ENV)
36
+ url = Insika::EnvSchema.read("INSIKA_DATABASE_URL", env)
37
+ if url && !url.empty?
38
+ pool = Insika::EnvSchema.read("INSIKA_DATABASE_POOL", env)
39
+ return Insika::Stores::Postgres.new(url: url, pool: pool && !pool.empty? ? Integer(pool) : 10)
40
+ end
41
+
29
42
  db = Insika::EnvSchema.read("INSIKA_DB", env)
30
- db && !db.empty? ? Insika::Stores::SQLite.new(path: db) : Insika::Stores::Memory.new
43
+ return Insika::Stores::Memory.new unless db && !db.empty?
44
+
45
+ slow = Insika::EnvSchema.read("INSIKA_SLOW_STORE_MS", env)
46
+ Insika::Stores::SQLite.new(path: db, autocheckpoint: env["LITESTREAM_REPLICA_URL"].to_s.empty?,
47
+ slow_ms: slow && Float(slow))
48
+ end
49
+
50
+ # On unless INSIKA_MODEL_VISIBLE_TRACES is set to a false value.
51
+ def model_visible_traces?(env = ENV)
52
+ value = Insika::EnvSchema.read("INSIKA_MODEL_VISIBLE_TRACES", env)
53
+ value.nil? || Insika::EnvSchema.truthy?(value)
31
54
  end
32
55
 
33
56
  # an integer tick knob from the env, nil when unset (the caller's
34
- # default wins). Dual-read honors the deprecated HARNESS_* alias.
57
+ # default wins).
35
58
  def tick_env(name, env = ENV)
36
59
  value = Insika::EnvSchema.read(name, env)
37
60
  value&.to_i
@@ -43,6 +66,8 @@ module Insika
43
66
  def spine(backend:, extra_policy_builtins: {})
44
67
  session_store = Insika::SessionStore.new(store: backend)
45
68
  task_store = Insika::TaskStore.new(store: backend)
69
+ llm_trace_store = Insika::LLMTraceStore.new(store: backend)
70
+ model_metrics_store = Insika::ModelMetricsStore.new(store: backend)
46
71
  checkpoint_store = Insika::CheckpointStore.new(store: backend)
47
72
  pending_action_store = Insika::PendingActionStore.new(store: backend)
48
73
  delegation_store = Insika::DelegationStore.new(store: backend)
@@ -122,7 +147,7 @@ module Insika
122
147
 
123
148
  Spine.new(
124
149
  backend: backend, event_stream: Insika::EventStream.new,
125
- session_store: session_store, task_store: task_store,
150
+ session_store: session_store, task_store: task_store, llm_trace_store: llm_trace_store, model_metrics_store: model_metrics_store,
126
151
  checkpoint_store: checkpoint_store, pending_action_store: pending_action_store,
127
152
  delegation_store: delegation_store,
128
153
  memory_store: memory_store, memory_audit_store: memory_audit_store,
@@ -168,6 +193,12 @@ module Insika
168
193
  # expose it. The gem's require lives IN the factory block (loaded on
169
194
  # the 1st instance, turn time -> wiring-load stays gem-free).
170
195
  register_artifact_tool(spine)
196
+ # Turns read profiles with the platform agent defaults applied; the
197
+ # authoring commands the roots register keep the agent's own record.
198
+ if (settings_store = executor_extra[:settings_store])
199
+ profiles = Insika::AgentDefaults::ProfileSource.new(Insika::ProfileSource.coerce(profiles),
200
+ settings_store: settings_store)
201
+ end
171
202
  spine.hooks.register(:task, after: guardrails.output_validator)
172
203
  middleware = Insika::MiddlewareStack.new([edge_limiter, guardrails.input_guardrail].compact)
173
204
 
@@ -192,11 +223,16 @@ module Insika
192
223
  # the profile's `reliability` data gates it, so the bare wiring is unchanged.
193
224
  reliability = Insika::Reliability.new(circuit_store: spine.circuit_state,
194
225
  event_stream: spine.event_stream)
226
+ if (url = Insika::Coercion.presence(Insika::EnvSchema.read("INSIKA_CONVERSATIONS_URL")))
227
+ executor_extra = {shared_conversations: Insika::SharedConversations.new(
228
+ url: url, token: Insika::EnvSchema.read("INSIKA_CONVERSATIONS_TOKEN"), store: spine.backend)}.merge(executor_extra)
229
+ end
195
230
  executor = Insika::Executor.new(
196
231
  context_builder: context_builder, policy_engine: policy_engine,
197
232
  middleware: middleware, hooks: spine.hooks,
198
233
  tool_registry: tool_registry, skill_catalog: skill_catalog, profiles: profiles,
199
234
  session_store: spine.session_store, task_store: spine.task_store,
235
+ llm_trace_store: spine.llm_trace_store, model_metrics_store: spine.model_metrics_store,
200
236
  checkpoint_store: spine.checkpoint_store, event_stream: spine.event_stream,
201
237
  workflow_registry: spine.workflow_registry, pending_action_store: spine.pending_action_store,
202
238
  capability_registry: spine.capability_registry, tool_catalog: tool_catalog,
@@ -213,11 +249,14 @@ module Insika
213
249
  # the model-visible trace rides executor_extra like the
214
250
  # context trace — a module graph that omits it still builds (nil =
215
251
  # parity). The base graph wires it over the same backend the context
216
- # trace uses.
217
- model_visible_trace_store: Insika::ModelVisibleTraceStore.new(store: spine.backend),
252
+ # trace uses. INSIKA_MODEL_VISIBLE_TRACES=0 stops the WRITE only (nothing in
253
+ # the product reads it back; it was ~80% of the bytes a turn wrote): the
254
+ # purge paths below keep their own store, so old records still go.
255
+ model_visible_trace_store: model_visible_traces? ? Insika::ModelVisibleTraceStore.new(store: spine.backend) : nil,
218
256
  # the per-turn extraction hook (nil = the loop is off,
219
257
  # parity). Gated per-agent by `profile.knowledge`.
220
258
  knowledge_store: spine.knowledge_store,
259
+ # llm stays graph-scoped; Executor binds instrumentation on operation copies.
221
260
  **executor_extra
222
261
  )
223
262
 
@@ -228,6 +267,10 @@ module Insika
228
267
  profiles: profiles, proposal_store: spine.proposal_store,
229
268
  session_store: spine.session_store, memory_store: spine.memory_store,
230
269
  settings_store: executor_extra[:settings_store],
270
+ distiller_factory: ->(config) {
271
+ Insika::Distill::DistillerFactory.build(config,
272
+ utility_model: executor_extra[:settings_store]&.get&.[]("utility_model"), llm: executor_extra[:llm])
273
+ },
231
274
  event_stream: spine.event_stream
232
275
  )
233
276
 
@@ -297,6 +340,11 @@ module Insika
297
340
  profiles: profiles, executor: executor, budget_ledger: spine.budget_ledger,
298
341
  event_stream: spine.event_stream
299
342
  )
343
+ # RubyLLM's model catalog (prices included) in the store, refreshed
344
+ # daily by one worker and reloaded by the rest.
345
+ executor.tick.model_registry = Insika::ModelRegistry.new(
346
+ store: spine.backend, interval: (tick_env("INSIKA_MODEL_REFRESH_HOURS") || 24) * 3600
347
+ )
300
348
  # the distillation engine — the tick-duty that finds idle
301
349
  # customer sessions and distills them on its own worker fiber (a
302
350
  # supervisor child, started in serving mode next to the tick).
@@ -323,7 +371,11 @@ module Insika
323
371
  skill_store: skill_catalog.store, # the harvest's dedup reads the authored skills
324
372
  tool_trace_store: executor_extra[:tool_trace_store],
325
373
  settings_store: executor_extra[:settings_store],
326
- negative_list: nil, miner_factory: nil,
374
+ negative_list: nil,
375
+ miner_factory: ->(config) {
376
+ Insika::Harvest::MinerFactory.build(config,
377
+ utility_model: executor_extra[:settings_store]&.get&.[]("utility_model"), llm: executor_extra[:llm])
378
+ },
327
379
  event_stream: spine.event_stream
328
380
  )
329
381
  executor.harvest_engine = Insika::HarvestEngine.new(
@@ -343,6 +395,7 @@ module Insika
343
395
  Graph::Result.new(
344
396
  backend: spine.backend, event_stream: spine.event_stream,
345
397
  session_store: spine.session_store, task_store: spine.task_store,
398
+ llm_trace_store: spine.llm_trace_store, model_metrics_store: spine.model_metrics_store,
346
399
  checkpoint_store: spine.checkpoint_store, pending_action_store: spine.pending_action_store,
347
400
  delegation_store: spine.delegation_store,
348
401
  memory_store: spine.memory_store, memory_audit_store: spine.memory_audit_store,
@@ -367,7 +420,8 @@ module Insika
367
420
  tool_catalog: tool_catalog, skill_catalog: skill_catalog, prompt_catalog: prompt_catalog,
368
421
  hooks: spine.hooks, guardrails: guardrails, middleware: middleware,
369
422
  context_providers: context_providers, context_builder: context_builder,
370
- policy_engine: policy_engine, profiles: profiles, executor: executor, bus: bus
423
+ # the agent's own records: the Studio and the roots author through them
424
+ policy_engine: policy_engine, profiles: Insika::AgentDefaults.own(profiles), executor: executor, bus: bus
371
425
  )
372
426
  end
373
427
 
@@ -422,6 +476,13 @@ module Insika
422
476
  bus = Insika::CommandBus.new
423
477
  bus.register(:create_session,
424
478
  Insika::Commands::CreateSession.new(session_store: spine.session_store, event_stream: spine.event_stream))
479
+ # A snapshot loaded into a conversation before its first turn — what an eval
480
+ # case declares as `state:`. The HTTP route and the in-process eval transport
481
+ # both dispatch this one command.
482
+ bus.register(:seed_session,
483
+ Insika::Commands::SeedSession.new(session_store: spine.session_store,
484
+ memory_store: spine.memory_store,
485
+ event_stream: spine.event_stream))
425
486
  bus.register(:cancel_task,
426
487
  Insika::Commands::CancelTask.new(task_store: spine.task_store, executor: executor))
427
488
  bus.register(:pause_task,
@@ -529,6 +590,11 @@ module Insika
529
590
  bus.register(:delete_artifact,
530
591
  Insika::Commands::DeleteArtifact.new(artifact_store: spine.artifact_store,
531
592
  event_stream: spine.event_stream))
593
+ # a person's verdict on a guardrail flag (false positive / resolved)
594
+ # — the Session viewer's button and the Review inbox.
595
+ bus.register(:review_guardrail_flag,
596
+ Insika::Commands::ReviewGuardrailFlag.new(session_store: spine.session_store,
597
+ event_stream: spine.event_stream))
532
598
  bus.register(:revoke_contact,
533
599
  Insika::Commands::RevokeContact.new(contact_store: spine.contact_store,
534
600
  followup_store: spine.followup_store,
@@ -554,7 +620,11 @@ module Insika
554
620
  skill_store: skill_catalog.store,
555
621
  tool_trace_store: executor_extra[:tool_trace_store],
556
622
  settings_store: executor_extra[:settings_store],
557
- negative_list: nil, miner_factory: nil,
623
+ negative_list: nil,
624
+ miner_factory: ->(config) {
625
+ Insika::Harvest::MinerFactory.build(config,
626
+ utility_model: executor_extra[:settings_store]&.get&.[]("utility_model"), llm: executor_extra[:llm])
627
+ },
558
628
  event_stream: spine.event_stream
559
629
  ))
560
630
  bus.register(:gate_harvest,
@@ -568,7 +638,7 @@ module Insika
568
638
  harvest_store: spine.harvest_store,
569
639
  skill_store: skill_catalog.store,
570
640
  skill_catalog: skill_catalog,
571
- profile_source: profiles, criterion: nil, conversion_gate: nil,
641
+ profile_source: Insika::AgentDefaults.own(profiles), criterion: nil, conversion_gate: nil,
572
642
  event_stream: spine.event_stream
573
643
  ))
574
644
  bus.register(:rollback_harvest,
@@ -576,7 +646,7 @@ module Insika
576
646
  harvest_store: spine.harvest_store,
577
647
  skill_store: skill_catalog.store,
578
648
  skill_catalog: skill_catalog,
579
- profile_source: profiles, event_stream: spine.event_stream
649
+ profile_source: Insika::AgentDefaults.own(profiles), event_stream: spine.event_stream
580
650
  ))
581
651
  bus.register(:reject_harvest,
582
652
  Insika::Commands::RejectHarvest.new(
@@ -594,7 +664,7 @@ module Insika
594
664
  # dispatches this bus command; nothing else writes the demo agent.
595
665
  config_store = Insika::ConfigStore.new(store: spine.backend)
596
666
  demo_seeder = Insika::Demo::Seeder.new(
597
- profiles: profiles, store: spine.backend, session_store: spine.session_store,
667
+ profiles: Insika::AgentDefaults.own(profiles), store: spine.backend, session_store: spine.session_store,
598
668
  task_store: spine.task_store, outcome_store: spine.outcome_store,
599
669
  funnel_store: spine.funnel_store, followup_store: spine.followup_store,
600
670
  refinement_store: spine.refinement_store, pending_action_store: spine.pending_action_store,
@@ -611,7 +681,7 @@ module Insika
611
681
  # Infra spine (phase 1 output). Value object — the roots read these to promote
612
682
  # them to their historic public constants (SESSION_STORE, REGISTRY, ...).
613
683
  Spine = Struct.new(
614
- :backend, :event_stream, :session_store, :task_store, :checkpoint_store,
684
+ :backend, :event_stream, :session_store, :task_store, :llm_trace_store, :model_metrics_store, :checkpoint_store,
615
685
  :pending_action_store, :delegation_store, :memory_store, :memory_audit_store,
616
686
  :refinement_store,
617
687
  :harvest_store,
@@ -629,7 +699,7 @@ module Insika
629
699
  # deployment's overlay, or the same code registry at the base).
630
700
  Result = Struct.new(
631
701
  :backend, :event_stream,
632
- :session_store, :task_store, :checkpoint_store, :pending_action_store, :delegation_store,
702
+ :session_store, :task_store, :llm_trace_store, :model_metrics_store, :checkpoint_store, :pending_action_store, :delegation_store,
633
703
  :memory_store, :memory_audit_store, :refinement_store, :outbox_store, :shadow_pair_store,
634
704
  :inbound_log, :token_store,
635
705
  :budget_ledger, :circuit_state, :outcome_store, :funnel_store,
@@ -646,7 +716,7 @@ module Insika
646
716
  :profiles, :executor, :bus,
647
717
  keyword_init: true
648
718
  ) do
649
- def durable? = backend.is_a?(Insika::Stores::SQLite)
719
+ def durable? = Insika::Stores.durable?(backend)
650
720
  end
651
721
 
652
722
  # The save_artifact tool, registered OPTIONAL on the shared code registry
@@ -678,7 +748,9 @@ module Insika
678
748
  require "ruby_llm"
679
749
  require_relative "../tools/run_persona_eval"
680
750
  Insika::Tools::RunPersonaEval.new(
681
- golden_store: golden_store, profiles: graph.profiles, tool_registry: graph.tool_registry,
751
+ golden_store: golden_store, tool_registry: graph.tool_registry,
752
+ # the target as its turns see it: kit tools count in the derived swap list
753
+ profiles: Insika::AgentDefaults.for_turns(graph.profiles, settings_store),
682
754
  runtime: GraphChat.new(graph: graph), graph: graph, settings_store: settings_store,
683
755
  budget_ledger: graph.budget_ledger, event_stream: graph.event_stream, llm: llm
684
756
  )
@@ -18,6 +18,10 @@ module Insika
18
18
  @graph = graph
19
19
  end
20
20
 
21
+ # The graph this seam speaks for — an eval transport reaches its bus and
22
+ # event stream through here.
23
+ attr_reader :graph
24
+
21
25
  # One turn, in-process -> the assistant's text. `agent:` is REQUIRED —
22
26
  # this seam has no notion of "the default agent" (that is a DSL::Runtime
23
27
  # concept, filled in by its own caller before delegating here). Raises