insika 0.9.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 (257) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +237 -2
  3. data/README.md +5 -3
  4. data/bin/insika +3 -0
  5. data/bin/insika-router +0 -1
  6. data/docs/AGENTS.md +90 -7
  7. data/docs/ARCHITECTURE.md +12 -7
  8. data/docs/ARTIFACTS.md +33 -3
  9. data/docs/BENCHMARK.md +50 -7
  10. data/docs/CONTEXT.md +63 -0
  11. data/docs/CROSS-HARNESS-BENCH.md +157 -0
  12. data/docs/DEPLOY.md +57 -11
  13. data/docs/EMBEDDING.md +10 -0
  14. data/docs/EVALS.md +33 -4
  15. data/docs/FACTS.md +8 -6
  16. data/docs/HARVEST.md +8 -3
  17. data/docs/KNOWLEDGE.md +7 -0
  18. data/docs/MEDIA.md +6 -2
  19. data/docs/OBSERVABILITY.md +143 -22
  20. data/docs/PERFORMANCE.md +110 -0
  21. data/docs/PLUGINS.md +4 -4
  22. data/docs/POLICY.md +22 -13
  23. data/docs/RELEASING.md +5 -0
  24. data/docs/ROUTER.md +7 -0
  25. data/docs/RUBYLLM_2_MIGRATION.md +307 -0
  26. data/docs/SECURITY.md +7 -0
  27. data/docs/SHARED-CONVERSATIONS.md +40 -0
  28. data/docs/SKILLS.md +5 -3
  29. data/docs/STUDIO.md +122 -0
  30. data/docs/TOOLS.md +111 -9
  31. data/docs/operate.md +3 -0
  32. data/lib/insika/agent_defaults.rb +140 -0
  33. data/lib/insika/agent_file_store.rb +12 -0
  34. data/lib/insika/agent_profile.rb +78 -8
  35. data/lib/insika/artifact_signing.rb +11 -0
  36. data/lib/insika/chat_builder.rb +79 -25
  37. data/lib/insika/checkpoint.rb +6 -1
  38. data/lib/insika/checkpoint_store.rb +59 -7
  39. data/lib/insika/commands/agent_payload.rb +3 -3
  40. data/lib/insika/commands/delete_kit.rb +26 -0
  41. data/lib/insika/commands/gate_harvest.rb +3 -0
  42. data/lib/insika/commands/review_guardrail_flag.rb +38 -0
  43. data/lib/insika/commands/run_harvest.rb +17 -1
  44. data/lib/insika/commands/seed_session.rb +11 -1
  45. data/lib/insika/commands/send_message.rb +9 -3
  46. data/lib/insika/commands/update_settings.rb +23 -1
  47. data/lib/insika/commands/write_kit.rb +27 -0
  48. data/lib/insika/compaction.rb +33 -7
  49. data/lib/insika/config_store.rb +87 -7
  50. data/lib/insika/context/priority.rb +3 -0
  51. data/lib/insika/context/provider.rb +2 -2
  52. data/lib/insika/context/providers/knowledge.rb +25 -2
  53. data/lib/insika/context/providers/memory.rb +53 -4
  54. data/lib/insika/context/providers/pending_confirmation.rb +52 -0
  55. data/lib/insika/context/providers/prompt.rb +17 -4
  56. data/lib/insika/context/providers/session.rb +39 -3
  57. data/lib/insika/context/reranker.rb +75 -0
  58. data/lib/insika/distill.rb +22 -13
  59. data/lib/insika/doctor.rb +34 -4
  60. data/lib/insika/dsl/runtime.rb +36 -4
  61. data/lib/insika/dsl/server_boot.rb +4 -12
  62. data/lib/insika/dsl.rb +13 -5
  63. data/lib/insika/edge_limiter.rb +23 -15
  64. data/lib/insika/egress_guard.rb +11 -3
  65. data/lib/insika/env_schema.rb +33 -73
  66. data/lib/insika/evals/assertions.rb +171 -16
  67. data/lib/insika/evals/golden.rb +141 -4
  68. data/lib/insika/evals/judge.rb +19 -10
  69. data/lib/insika/evals/moderator_compare.rb +47 -0
  70. data/lib/insika/evals/report.rb +61 -9
  71. data/lib/insika/evals/runner.rb +44 -4
  72. data/lib/insika/evals/simulator.rb +10 -1
  73. data/lib/insika/evals/transport.rb +200 -0
  74. data/lib/insika/event_stream.rb +63 -7
  75. data/lib/insika/evidence.rb +29 -8
  76. data/lib/insika/executor.rb +668 -141
  77. data/lib/insika/harvest.rb +26 -14
  78. data/lib/insika/harvest_engine.rb +1 -0
  79. data/lib/insika/harvest_store.rb +12 -0
  80. data/lib/insika/http_client.rb +87 -4
  81. data/lib/insika/kits.rb +54 -0
  82. data/lib/insika/knowledge.rb +34 -20
  83. data/lib/insika/llm_configurator.rb +46 -4
  84. data/lib/insika/llm_provider_store.rb +1 -3
  85. data/lib/insika/llm_trace_store.rb +54 -0
  86. data/lib/insika/mcp_client.rb +27 -63
  87. data/lib/insika/mcp_live_tool.rb +55 -9
  88. data/lib/insika/mcp_store.rb +32 -1
  89. data/lib/insika/mcp_tool_registry.rb +69 -26
  90. data/lib/insika/media.rb +38 -25
  91. data/lib/insika/memory_store.rb +4 -3
  92. data/lib/insika/memory_trim.rb +60 -0
  93. data/lib/insika/middleware.rb +12 -3
  94. data/lib/insika/model_metrics_store.rb +285 -0
  95. data/lib/insika/model_registry.rb +110 -0
  96. data/lib/insika/model_resolver.rb +1 -1
  97. data/lib/insika/model_selection.rb +14 -21
  98. data/lib/insika/model_visible.rb +17 -6
  99. data/lib/insika/onboarding.rb +1 -0
  100. data/lib/insika/outcome_store.rb +1 -1
  101. data/lib/insika/overlay_tool_registry.rb +25 -2
  102. data/lib/insika/pack_importer.rb +54 -9
  103. data/lib/insika/packaging.rb +14 -5
  104. data/lib/insika/pending_action_store.rb +45 -18
  105. data/lib/insika/plugin/loader.rb +4 -2
  106. data/lib/insika/profile_source.rb +4 -0
  107. data/lib/insika/provider_error_classifier.rb +18 -3
  108. data/lib/insika/queue_policy.rb +7 -2
  109. data/lib/insika/refinement/proposer.rb +42 -17
  110. data/lib/insika/reliability.rb +12 -3
  111. data/lib/insika/router/app.rb +14 -5
  112. data/lib/insika/router/session_key.rb +35 -5
  113. data/lib/insika/safety/config.rb +20 -3
  114. data/lib/insika/safety/corpus.rb +10 -3
  115. data/lib/insika/safety/factory.rb +39 -1
  116. data/lib/insika/safety/input_guardrail.rb +21 -1
  117. data/lib/insika/safety/judge_moderator.rb +70 -0
  118. data/lib/insika/safety/output_validator.rb +29 -5
  119. data/lib/insika/safety/prompt_echo.rb +89 -0
  120. data/lib/insika/schema_guard.rb +53 -76
  121. data/lib/insika/server/app.rb +17 -8
  122. data/lib/insika/server/responses.rb +19 -1
  123. data/lib/insika/session_actor.rb +58 -28
  124. data/lib/insika/session_store.rb +159 -10
  125. data/lib/insika/settings_store.rb +47 -1
  126. data/lib/insika/shared_conversations.rb +305 -0
  127. data/lib/insika/skill_catalog.rb +28 -2
  128. data/lib/insika/skill_store.rb +6 -0
  129. data/lib/insika/soak/runner.rb +1 -1
  130. data/lib/insika/steer_injector.rb +7 -48
  131. data/lib/insika/store.rb +29 -1
  132. data/lib/insika/stores/json_value.rb +37 -0
  133. data/lib/insika/stores/memory.rb +22 -35
  134. data/lib/insika/stores/postgres.rb +295 -0
  135. data/lib/insika/stores/sqlite.rb +257 -43
  136. data/lib/insika/stores/turn_counter.rb +25 -0
  137. data/lib/insika/studio/app.rb +1453 -161
  138. data/lib/insika/studio/assets/dist/application.css +1 -1
  139. data/lib/insika/studio/assets/dist/application.js +25 -25
  140. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  141. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  142. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  143. data/lib/insika/studio/assets/dist/fonts/ibm-plex-sans-latin-wght-normal.woff2 +0 -0
  144. data/lib/insika/studio/forms.rb +171 -9
  145. data/lib/insika/studio/nav_icons.rb +3 -0
  146. data/lib/insika/studio/views/_agent_config_retrieval.erb +73 -0
  147. data/lib/insika/studio/views/_agent_picker.erb +15 -0
  148. data/lib/insika/studio/views/_agent_tab_config.erb +78 -242
  149. data/lib/insika/studio/views/_agent_tab_history.erb +56 -1
  150. data/lib/insika/studio/views/_agent_tab_performance.erb +34 -0
  151. data/lib/insika/studio/views/_agent_tab_prompts.erb +7 -9
  152. data/lib/insika/studio/views/_agent_tab_skills.erb +16 -7
  153. data/lib/insika/studio/views/_agent_tab_tools.erb +90 -0
  154. data/lib/insika/studio/views/_agents_master.erb +8 -12
  155. data/lib/insika/studio/views/_cfg_alerts.erb +4 -0
  156. data/lib/insika/studio/views/_cfg_budget.erb +20 -0
  157. data/lib/insika/studio/views/_cfg_distill.erb +26 -0
  158. data/lib/insika/studio/views/_cfg_followup.erb +30 -0
  159. data/lib/insika/studio/views/_cfg_grounding.erb +21 -0
  160. data/lib/insika/studio/views/_cfg_guardrails.erb +47 -0
  161. data/lib/insika/studio/views/_cfg_harvest.erb +35 -0
  162. data/lib/insika/studio/views/_cfg_reliability.erb +32 -0
  163. data/lib/insika/studio/views/_chats_master.erb +66 -0
  164. data/lib/insika/studio/views/_customers_master.erb +37 -0
  165. data/lib/insika/studio/views/_empty.erb +15 -0
  166. data/lib/insika/studio/views/_facts_master.erb +42 -0
  167. data/lib/insika/studio/views/_harvest_candidate_detail.erb +14 -0
  168. data/lib/insika/studio/views/_identity.erb +16 -0
  169. data/lib/insika/studio/views/_kpi.erb +17 -0
  170. data/lib/insika/studio/views/_message.erb +40 -2
  171. data/lib/insika/studio/views/_model_chart.erb +82 -0
  172. data/lib/insika/studio/views/_refinement_master.erb +38 -0
  173. data/lib/insika/studio/views/{tool_edit.erb → _tool_edit.erb} +32 -9
  174. data/lib/insika/studio/views/_tool_users.erb +17 -0
  175. data/lib/insika/studio/views/_why.erb +53 -0
  176. data/lib/insika/studio/views/agent_detail.erb +17 -10
  177. data/lib/insika/studio/views/agents.erb +28 -16
  178. data/lib/insika/studio/views/approvals.erb +16 -11
  179. data/lib/insika/studio/views/artifact.erb +20 -7
  180. data/lib/insika/studio/views/artifacts.erb +19 -14
  181. data/lib/insika/studio/views/chats.erb +22 -29
  182. data/lib/insika/studio/views/customer.erb +88 -43
  183. data/lib/insika/studio/views/customers.erb +16 -26
  184. data/lib/insika/studio/views/evals.erb +49 -31
  185. data/lib/insika/studio/views/fact_detail.erb +114 -0
  186. data/lib/insika/studio/views/facts.erb +15 -116
  187. data/lib/insika/studio/views/followups.erb +63 -52
  188. data/lib/insika/studio/views/funnel.erb +104 -43
  189. data/lib/insika/studio/views/harvest.erb +85 -59
  190. data/lib/insika/studio/views/home.erb +138 -23
  191. data/lib/insika/studio/views/inbox.erb +36 -0
  192. data/lib/insika/studio/views/kits.erb +176 -0
  193. data/lib/insika/studio/views/knowledge.erb +92 -80
  194. data/lib/insika/studio/views/layout.erb +42 -11
  195. data/lib/insika/studio/views/live.erb +45 -0
  196. data/lib/insika/studio/views/mcp.erb +43 -13
  197. data/lib/insika/studio/views/models.erb +187 -0
  198. data/lib/insika/studio/views/parity.erb +38 -32
  199. data/lib/insika/studio/views/playground.erb +13 -9
  200. data/lib/insika/studio/views/refinement.erb +15 -224
  201. data/lib/insika/studio/views/refinement_detail.erb +243 -0
  202. data/lib/insika/studio/views/session.erb +152 -117
  203. data/lib/insika/studio/views/settings.erb +86 -22
  204. data/lib/insika/studio/views/skills.erb +53 -19
  205. data/lib/insika/studio/views/system_files.erb +7 -7
  206. data/lib/insika/studio/views/task.erb +45 -15
  207. data/lib/insika/studio/views/tasks.erb +35 -18
  208. data/lib/insika/studio/views/tools.erb +62 -73
  209. data/lib/insika/task_actor.rb +4 -1
  210. data/lib/insika/task_store.rb +67 -1
  211. data/lib/insika/telemetry/pricing.rb +16 -14
  212. data/lib/insika/telemetry/recorder.rb +103 -9
  213. data/lib/insika/telemetry/ruby_llm_instrumenter.rb +110 -0
  214. data/lib/insika/telemetry.rb +12 -2
  215. data/lib/insika/templates/daily-digest/README.md +13 -0
  216. data/lib/insika/templates/daily-digest/agent.rb +22 -11
  217. data/lib/insika/testing/store_contract.rb +65 -0
  218. data/lib/insika/text_tool_call.rb +66 -0
  219. data/lib/insika/tick.rb +7 -1
  220. data/lib/insika/token_store.rb +5 -0
  221. data/lib/insika/tool_batch.rb +1 -1
  222. data/lib/insika/tool_catalog.rb +12 -2
  223. data/lib/insika/tool_definition.rb +10 -4
  224. data/lib/insika/tool_envelope.rb +165 -28
  225. data/lib/insika/tool_manifest.rb +1 -0
  226. data/lib/insika/tool_store.rb +3 -0
  227. data/lib/insika/tool_trace_store.rb +15 -8
  228. data/lib/insika/tool_usage_report.rb +2 -2
  229. data/lib/insika/tools/a2a_remote.rb +1 -1
  230. data/lib/insika/tools/agent_enum.rb +1 -1
  231. data/lib/insika/tools/confirm_pending.rb +112 -0
  232. data/lib/insika/tools/data_defined_tool.rb +23 -15
  233. data/lib/insika/tools/generate_image.rb +2 -2
  234. data/lib/insika/tools/load_knowledge.rb +7 -3
  235. data/lib/insika/tools/load_skill.rb +3 -3
  236. data/lib/insika/tools/present.rb +1 -8
  237. data/lib/insika/tools/remember.rb +7 -2
  238. data/lib/insika/tools/run_persona_eval.rb +3 -5
  239. data/lib/insika/tools/save_artifact.rb +16 -6
  240. data/lib/insika/tools/schedule_followup.rb +3 -3
  241. data/lib/insika/tools/stuck_signal.rb +3 -3
  242. data/lib/insika/tools/subagent.rb +5 -5
  243. data/lib/insika/tools/subagents.rb +3 -3
  244. data/lib/insika/tools/tool_search.rb +7 -5
  245. data/lib/insika/tools/tts.rb +2 -2
  246. data/lib/insika/tools/update_briefing.rb +4 -4
  247. data/lib/insika/turn_output.rb +2 -2
  248. data/lib/insika/turn_state.rb +18 -0
  249. data/lib/insika/turn_timing.rb +51 -14
  250. data/lib/insika/version.rb +1 -1
  251. data/lib/insika/wiring/graph.rb +81 -16
  252. data/lib/insika/workflow.rb +25 -64
  253. data/lib/insika.rb +19 -3
  254. metadata +69 -13
  255. data/lib/insika/commands/import_mcp_tools.rb +0 -48
  256. data/lib/insika/mcp_http_client.rb +0 -67
  257. data/lib/insika/mcp_tool_ingestor.rb +0 -165
@@ -14,13 +14,13 @@ module Insika
14
14
  # loads it lazily inside create_chat.
15
15
  class LoadSkill < RubyLLM::Tool
16
16
  description "Loads the complete instructions (SKILL.md) of a skill by name"
17
- param :name, desc: "Exact skill name, as listed in <available_skills>"
17
+ parameter :name, description: "Exact skill name, as listed in <available_skills>"
18
18
 
19
19
  # RubyLLM::Tool#name derives from self.class.name — for a nested class it produces
20
20
  # "insika--tools--load_skill", not "load_skill" (which wire_callbacks/
21
21
  # :skill_activated and SkillCatalog#format_for_prompt assume). Explicit
22
22
  # override. Coexists with
23
- # `param :name` (verified: the param is still present).
23
+ # `parameter :name` (verified: the param is still present).
24
24
  def name = "load_skill"
25
25
 
26
26
  # trace_recorder/state are OPTIONAL (nil = no trace, parity): this tool is
@@ -86,7 +86,7 @@ module Insika
86
86
 
87
87
  @trace_recorder.record(
88
88
  session_id: @state.task.session_id,
89
- entry: { "turn" => @state.turn, "tool" => "load_skill", "call_id" => "",
89
+ entry: { "task_id" => @state.task.id.to_s, "turn" => @state.turn, "tool" => "load_skill", "call_id" => "",
90
90
  "args" => { "name" => name.to_s }, "result" => result,
91
91
  "ms" => ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round,
92
92
  "at" => Time.now.utc.iso8601 }
@@ -41,14 +41,7 @@ module Insika
41
41
 
42
42
  def name = @definition.name
43
43
  def description = @definition.description
44
- def params_schema = @definition.parameters
45
-
46
- def parameters
47
- @parameters ||= @definition.top_level_params.each_with_object({}) do |p, acc|
48
- sym = p[:name].to_sym
49
- acc[sym] = RubyLLM::Parameter.new(sym, type: p[:type], desc: p[:description], required: p[:required])
50
- end
51
- end
44
+ def parameters_schema = @definition.parameters
52
45
 
53
46
  def execute(**kwargs)
54
47
  spec = @definition.presentation
@@ -13,8 +13,8 @@ module Insika
13
13
  description "Stores information to remember in future conversations. Use `key` " \
14
14
  "for a durable key-value fact (overwrites the previous one); omit " \
15
15
  "`key` for a free-form note."
16
- param :value, desc: "The content to remember"
17
- param :key, desc: "Fact key (e.g.: 'plan', 'name'); omit for a note", required: false
16
+ parameter :value, description: "The content to remember"
17
+ parameter :key, description: "Fact key (e.g.: 'plan', 'name'); omit for a note", required: false
18
18
 
19
19
  # otherwise RubyLLM derives "insika--tools--remember" from the class name.
20
20
  def name = "remember"
@@ -28,6 +28,11 @@ module Insika
28
28
  end
29
29
 
30
30
  def execute(value:, key: nil)
31
+ if @state.shared_conversations
32
+ kind = key.to_s.strip.empty? ? 'note' : 'fact'
33
+ record = @state.shared_conversations.propose_memory(task:@state.task,id:kind == 'note' ? SecureRandom.uuid : key.to_s,value:value.to_s,kind:kind)
34
+ return {proposed:kind,id:record['id'],revision:record['revision']}
35
+ end
31
36
  if key.to_s.strip.empty?
32
37
  note = @store.add_note(tenant: @tenant, text: value.to_s)
33
38
  emit("note", note.id)
@@ -45,7 +45,7 @@ module Insika
45
45
  "its target agent and score the whole conversation with the configured " \
46
46
  "judge panel. Refuses if the target exposes a tool that could write for " \
47
47
  "real."
48
- param :case_id, desc: "Id of the persona case to run"
48
+ parameter :case_id, description: "Id of the persona case to run"
49
49
 
50
50
  def name = "run_persona_eval"
51
51
 
@@ -111,7 +111,7 @@ module Insika
111
111
  "#{super} Cases you may run: #{ids.join(', ')}."
112
112
  end
113
113
 
114
- def params_schema
114
+ def parameters_schema
115
115
  Insika::Tools::AgentEnum.inject(super, case_ids, path: %i[case_id])
116
116
  end
117
117
 
@@ -319,9 +319,7 @@ module Insika
319
319
  return if @budget_ledger.nil? || meter.empty?
320
320
 
321
321
  tokens = meter.sum do |m|
322
- m.input_tokens.to_i + m.output_tokens.to_i +
323
- (m.respond_to?(:cached_tokens) ? m.cached_tokens.to_i : 0) +
324
- (m.respond_to?(:cache_creation_tokens) ? m.cache_creation_tokens.to_i : 0)
322
+ m.tokens.input.to_i + m.tokens.output.to_i + m.tokens.cache_read.to_i + m.tokens.cache_write.to_i
325
323
  end
326
324
  return if tokens.zero?
327
325
 
@@ -17,11 +17,21 @@ module Insika
17
17
  class SaveArtifact < RubyLLM::Tool
18
18
  description "Save an HTML (or markdown / SVG) page — a report, a digest, " \
19
19
  "a summary — and get back a URL to share. Use when the answer " \
20
- "is a document the channel message cannot carry."
21
- param :title, desc: "Short human-readable title (<= 200 chars)"
22
- param :content, desc: "The page content: HTML with inline SVG for charts, " \
23
- "markdown, or SVG. No scripts, no external resources."
24
- param :mime, desc: "text/html (default), text/markdown, or image/svg+xml",
20
+ "is a document the channel message cannot carry. Executive reports " \
21
+ "need a clear title, period, generation date, evidence-backed takeaway, " \
22
+ "sources and limitations."
23
+ parameter :title, description: "Short human-readable title (<= 200 chars)"
24
+ parameter :content, description: "HTML, markdown, or SVG. HTML reports: use warm neutrals " \
25
+ "and one amber or merchant accent, consistent type/spacing/KPI cards, " \
26
+ "responsive SVG or HTML charts with labels, units and exact values/table. " \
27
+ "Use CSS hover/focus highlights and hoverable tooltips in reserved space, " \
28
+ "labeled native radio chart/table " \
29
+ "choices and details; keep values accessible on touch and focus " \
30
+ "visible. Support system dark mode, print with full tables/details, " \
31
+ "and 140ms transitions disabled for reduced motion. Date the data " \
32
+ "snapshot; never imply live updates. No scripts, event handlers, " \
33
+ "forms, external resources or decorative animation."
34
+ parameter :mime, description: "text/html (default), text/markdown, or image/svg+xml",
25
35
  required: false
26
36
 
27
37
  def name = "save_artifact"
@@ -92,4 +102,4 @@ module Insika
92
102
  end
93
103
  end
94
104
  end
95
- end
105
+ end
@@ -20,8 +20,8 @@ module Insika
20
20
  "when the customer agrees to be contacted again (a product, a " \
21
21
  "cart, a pending payment). The cancellation policy is permanent: " \
22
22
  "a customer who opted out can never be rescheduled."
23
- param :at, desc: "ISO 8601 (absolute) or relative '+6h' / '+2d'"
24
- param :reason, desc: "Short machine-readable reason, e.g. 'pix pending, " \
23
+ parameter :at, description: "ISO 8601 (absolute) or relative '+6h' / '+2d'"
24
+ parameter :reason, description: "Short machine-readable reason, e.g. 'pix pending, " \
25
25
  "customer said she would pay tonight'"
26
26
 
27
27
  def name = "schedule"
@@ -127,7 +127,7 @@ module Insika
127
127
  class CancelFollowup < RubyLLM::Tool
128
128
  description "Cancel a previously scheduled follow-up by its id. A follow-up " \
129
129
  "that already fired cannot be cancelled."
130
- param :id, desc: "The id returned by the schedule tool"
130
+ parameter :id, description: "The id returned by the schedule tool"
131
131
 
132
132
  def name = "cancel_followup"
133
133
 
@@ -19,8 +19,8 @@ module Insika
19
19
  description "Signal that you cannot proceed and end the turn. Use when the " \
20
20
  "request is out of your scope, you lack the data to help, or a human " \
21
21
  "must take over. Write your final sentence to the customer first."
22
- param :reason, desc: "Why you cannot proceed (goes to the operator, not the customer)"
23
- param :message, desc: "Optional final message if you wrote none", required: false
22
+ parameter :reason, description: "Why you cannot proceed (goes to the operator, not the customer)"
23
+ parameter :message, description: "Optional final message if you wrote none", required: false
24
24
 
25
25
  def name = "signal_stuck"
26
26
 
@@ -34,7 +34,7 @@ module Insika
34
34
  # A Halt ends the tool loop here, so the turn cannot continue after declaring
35
35
  # stuck. The payload's `say` is the fallback final message when the model
36
36
  # wrote no lead-in (the executor's halt_answer already prefers the lead-in).
37
- RubyLLM::Tool::Halt.new(Insika::ToolDefinition.wrap_halt(
37
+ Insika::ToolDefinition::Halt.new(Insika::ToolDefinition.wrap_halt(
38
38
  { "reason" => reason.to_s },
39
39
  message.to_s
40
40
  ))
@@ -24,10 +24,10 @@ module Insika
24
24
  "it BLOCKS and returns the child's final answer. Set async:true to " \
25
25
  "fire-and-forget a long task: it returns immediately and the child's " \
26
26
  "result arrives later as a new message on this conversation."
27
- param :agent, desc: "Id of the child agent to delegate to (must be one this agent may spawn)"
28
- param :message, desc: "The self-contained task/prompt for the child agent"
29
- param :async, type: :boolean, required: false,
30
- desc: "true = dispatch and continue (result delivered later); default false = wait for the answer"
27
+ parameter :agent, description: "Id of the child agent to delegate to (must be one this agent may spawn)"
28
+ parameter :message, description: "The self-contained task/prompt for the child agent"
29
+ parameter :async, type: :boolean, required: false,
30
+ description: "true = dispatch and continue (result delivered later); default false = wait for the answer"
31
31
 
32
32
  # otherwise RubyLLM derives "insika--tools--subagent" from the class name.
33
33
  def name = "spawn_subagent"
@@ -51,7 +51,7 @@ module Insika
51
51
  "#{super} Agents you may spawn: #{@allowed.join(', ')}."
52
52
  end
53
53
 
54
- def params_schema
54
+ def parameters_schema
55
55
  @agent_enum_schema ||= Insika::Tools::AgentEnum.inject(super, @allowed, path: %i[agent])
56
56
  end
57
57
 
@@ -20,9 +20,9 @@ module Insika
20
20
  "subtasks whose results you'll combine: it runs them concurrently " \
21
21
  "(much faster). Each child runs in an ISOLATED context, so put " \
22
22
  "everything it needs in its `message`."
23
- # array-of-objects param via the explicit JSON-schema form (the `param` DSL
23
+ # array-of-objects param via the explicit JSON-schema form (the `parameter` DSL
24
24
  # only reaches strings/scalars). Top-level `tasks` arrives as a kwarg to execute.
25
- params(
25
+ parameters(
26
26
  type: "object",
27
27
  properties: {
28
28
  tasks: {
@@ -60,7 +60,7 @@ module Insika
60
60
  "#{super} Agents you may spawn: #{@allowed.join(', ')}."
61
61
  end
62
62
 
63
- def params_schema
63
+ def parameters_schema
64
64
  @agent_enum_schema ||= Insika::Tools::AgentEnum.inject(super, @allowed, path: %i[tasks agent])
65
65
  end
66
66
 
@@ -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
@@ -81,11 +81,13 @@ module Insika
81
81
 
82
82
  def describe(entry)
83
83
  tool = @tool_registry.resolve(entry.name)
84
+ schema = Coercion.deep_stringify(tool.parameters_schema || {})
84
85
  {
85
86
  name: entry.name,
86
87
  description: entry.description,
87
- parameters: tool.parameters.transform_values do |p|
88
- { 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) }]
89
91
  end
90
92
  }
91
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,
@@ -111,7 +115,9 @@ module Insika
111
115
  # to serialize the turn's real exchange — user + assistant(tool_calls) + tool
112
116
  # results + final assistant — into the transcript. nil = no chat recorded
113
117
  # (workflow/halt) → persist_turn falls back to the {user, assistant} pair.
118
+ attr_accessor :shared_conversations
114
119
  attr_accessor :chat_baseline
120
+ attr_accessor :tool_halt
115
121
 
116
122
  # Internal: impl_name(String) -> STABLE name of the capability that
117
123
  # resolved it, computed by resolve_capabilities BEFORE the policy_request and
@@ -141,6 +147,9 @@ module Insika
141
147
  # Internal (memory): the turn's tenant (from the Command), scope of the write path
142
148
  # (`remember` tool). Set in run_pipeline; nil = DEFAULT_TENANT in the MemoryStore.
143
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
144
153
 
145
154
  # Internal: turn context deposited into the data-tools to
146
155
  # resolve {{ctx.*}} (chat_id/agent_id/tenant/store_id) and emit
@@ -155,6 +164,9 @@ module Insika
155
164
  # (workflow) or provider without counts.
156
165
  attr_accessor :usage
157
166
 
167
+ # Native rerank usage events, separate from the chat/Responses token totals.
168
+ attr_accessor :rerank_usage
169
+
158
170
  # Internal (Tool Search): ids of side-effects already completed in the
159
171
  # interrupted turn, propagated to the tools PROMOTED by tool_search (the same `skip`
160
172
  # that the eager tools' wrap_tools receives). Set in run_pipeline;
@@ -166,6 +178,12 @@ module Insika
166
178
  # creates the PendingAction/suspends/waits; `actor` = the turn's mailbox (used
167
179
  # by the coordinator for await(:approval)).
168
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
169
187
 
170
188
  # Internal: the turn's shared in-flight cap for tool calls —
171
189
  # ONE Async::Semaphore(tool_concurrency), installed by ToolAssembly#wrap_tools
@@ -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.9.0"
4
+ VERSION = "1.0.1"
5
5
  end