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
@@ -72,7 +72,7 @@ module Insika
72
72
  stats = tool_stats(sessions[id] || [], cutoff)
73
73
  never_called_rows(id, record, stats) +
74
74
  error_rate_rows(id, stats, days) +
75
- stale_rows(id, stats)
75
+ stale_rows(id, stats) + blocked_rows(id, stats, days)
76
76
  end
77
77
 
78
78
  Report.new(generated_at: now.iso8601, days: days,
@@ -87,8 +87,8 @@ module Insika
87
87
  # workflows without a chat) contributes nothing.
88
88
  def sessions_by_agent
89
89
  acc = Hash.new { |h, k| h[k] = [] }
90
- @task_store.each_id do |task_id|
91
- task = @task_store.find(task_id) or next
90
+ tasks = @task_store.respond_to?(:all) ? @task_store.all : @task_store.each_id.filter_map { |id| @task_store.find(id) }
91
+ tasks.each do |task|
92
92
  agent = task.command.is_a?(Hash) ? task.command.dig("payload", "agent") : nil
93
93
  next if agent.to_s.empty? || task.session_id.to_s.empty?
94
94
 
@@ -100,7 +100,7 @@ module Insika
100
100
  # tool name -> { calls:, errors:, window_calls:, window_errors:, last_at: }
101
101
  # over every stored trace entry of the agent's sessions.
102
102
  def tool_stats(session_ids, cutoff)
103
- stats = Hash.new { |h, k| h[k] = { calls: 0, errors: 0, window_calls: 0, window_errors: 0, last_at: nil } }
103
+ stats = Hash.new { |h, k| h[k] = { calls: 0, errors: 0, window_calls: 0, window_errors: 0, blocked: Hash.new(0), last_at: nil } }
104
104
  session_ids.each do |sid|
105
105
  @tool_trace_store.for_session(sid).each do |entry|
106
106
  s = stats[entry["tool"].to_s]
@@ -113,6 +113,7 @@ module Insika
113
113
 
114
114
  s[:window_calls] += 1
115
115
  s[:window_errors] += 1 if error
116
+ s[:blocked][entry["gate"]] += 1 if entry["gate"]
116
117
  end
117
118
  end
118
119
  stats
@@ -142,6 +143,15 @@ module Insika
142
143
  end
143
144
  end
144
145
 
146
+ def blocked_rows(agent, stats, days)
147
+ stats.flat_map do |tool, s|
148
+ s[:blocked].map do |gate, count|
149
+ Row.new(agent: agent, tool: tool, kind: "blocked",
150
+ detail: "#{count} call(s) blocked by #{gate} in the last #{days} day(s)")
151
+ end
152
+ end
153
+ end
154
+
145
155
  def stale_rows(agent, stats)
146
156
  stats.filter_map do |tool, s|
147
157
  next if s[:window_calls].positive? || s[:last_at].nil?
@@ -9,7 +9,7 @@ module Insika
9
9
  # enter lib/insika.rb; it is loaded in the registration block (wiring) on the 1st
10
10
  # instance. One instance per remote agent (its own name/description).
11
11
  class A2ARemote < RubyLLM::Tool
12
- param :message, desc: "The message/task for the remote agent"
12
+ parameter :message, description: "The message/task for the remote agent"
13
13
 
14
14
  def initialize(client:, url:, tool_name:, description:, event_stream: nil)
15
15
  @client = client
@@ -11,7 +11,7 @@ module Insika
11
11
  # the provider sees.
12
12
  #
13
13
  # Tolerant by construction: JSON-schema hashes reach us with symbol keys (the
14
- # class-level `params(...)` form) or string keys (anything that round-tripped
14
+ # class-level `parameters(...)` form) or string keys (anything that round-tripped
15
15
  # through JSON), and a shape we do not recognize is returned UNTOUCHED — a
16
16
  # schema we failed to annotate still works, one we corrupted would break every
17
17
  # delegation.
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_llm"
4
+
5
+ module Insika
6
+ module Tools
7
+ # What the two confirmation tools share: finding the customer's hold and
8
+ # refusing anyone else's — another conversation's, the operator's, or one
9
+ # already decided. Errors go back to the MODEL as bodies, never as
10
+ # exceptions: a tool exception never ends the turn.
11
+ module Confirmation
12
+ CUSTOMER = PendingActionStore::CUSTOMER
13
+
14
+ module_function
15
+
16
+ # -> [PendingAction, nil] | [nil, String]
17
+ def open_hold(state, pending_id)
18
+ store = state.pending_action_store
19
+ pending = store&.find(pending_id.to_s)
20
+ return [nil, "no held action with pending_id #{pending_id}"] if pending.nil?
21
+ return [nil, "#{pending_id} is not a customer confirmation"] unless pending.kind == CUSTOMER
22
+ return [nil, "#{pending_id} belongs to another conversation"] unless pending.session_id.to_s == state.task.session_id.to_s
23
+ # The hold and the answer to it are two customer messages, which is two
24
+ # TASKS. A hold THIS task created has not been put to the customer yet —
25
+ # the reply carrying it has not even been sent. Deciding it here would be
26
+ # the model confirming itself, and the write would land with nobody's word
27
+ # behind it.
28
+ return [nil, "#{pending_id} was held on this same turn; the customer has not answered it yet"] if pending.task_id.to_s == state.task.id.to_s
29
+ return [nil, "#{pending_id} was already #{pending.status}"] unless pending.status == :pending
30
+
31
+ [pending, nil]
32
+ end
33
+
34
+ # The turn's enveloped instance of the held tool — the same wrapper the
35
+ # hold came from, so the confirmed run pays every check but the hold.
36
+ def held_tool(state, name)
37
+ Array(state.allowed_tools).find do |tool|
38
+ tool.respond_to?(:call_confirmed) &&
39
+ (tool.name.to_s == name || (tool.respond_to?(:impl_name) && tool.impl_name.to_s == name))
40
+ end
41
+ end
42
+
43
+ def emit(event_stream, state, type, pending)
44
+ event_stream&.emit(Insika::Event.new(
45
+ type: type,
46
+ data: { pending_id: pending.id, tool: pending.tool },
47
+ meta: { task_id: state.task.id, session_id: state.task.session_id }
48
+ ))
49
+ end
50
+ end
51
+
52
+ # Runs an action the engine held for the customer's confirmation. The
53
+ # decision is written FIRST (durable — a crash between the two re-executes
54
+ # the turn and finds it approved), the original tool runs SECOND through its
55
+ # own envelope with the arguments recorded at the hold: there is no path to
56
+ # confirm different arguments than the ones the customer was shown.
57
+ # System builtin (like remember): `require "ruby_llm"` stays in THIS file,
58
+ # loaded lazily by the Executor in create_chat.
59
+ class ConfirmPending < RubyLLM::Tool
60
+ description "Runs an action the customer has just confirmed. Call it only when the customer's " \
61
+ "message agrees to exactly what was proposed; a change, a question or a new request " \
62
+ "is cancel_pending instead."
63
+ parameter :pending_id, description: "The pending_id the held action returned"
64
+
65
+ def name = "confirm_pending"
66
+
67
+ def initialize(event_stream:, state:)
68
+ @event_stream = event_stream
69
+ @state = state
70
+ super()
71
+ end
72
+
73
+ def execute(pending_id:)
74
+ pending, error = Confirmation.open_hold(@state, pending_id)
75
+ return { error: error } if error
76
+
77
+ tool = Confirmation.held_tool(@state, pending.tool)
78
+ return { error: "#{pending.tool} is not available on this turn" } unless tool
79
+
80
+ @state.pending_action_store.resolve(pending.id, decision: :approved, operator: Confirmation::CUSTOMER)
81
+ Confirmation.emit(@event_stream, @state, :confirmation_confirmed, pending)
82
+ tool.call_confirmed(pending.args || {})
83
+ end
84
+ end
85
+
86
+ # Drops a held action: the customer declined, changed something, or asked
87
+ # for anything else. The hold would expire on its own at the end of this
88
+ # turn; cancelling says so now, and lets the model answer accordingly.
89
+ class CancelPending < RubyLLM::Tool
90
+ description "Drops an action that was waiting for the customer's confirmation, because they " \
91
+ "declined, changed something, or asked for something else."
92
+ parameter :pending_id, description: "The pending_id the held action returned"
93
+
94
+ def name = "cancel_pending"
95
+
96
+ def initialize(event_stream:, state:)
97
+ @event_stream = event_stream
98
+ @state = state
99
+ super()
100
+ end
101
+
102
+ def execute(pending_id:)
103
+ pending, error = Confirmation.open_hold(@state, pending_id)
104
+ return { error: error } if error
105
+
106
+ @state.pending_action_store.resolve(pending.id, decision: :rejected, operator: Confirmation::CUSTOMER)
107
+ Confirmation.emit(@event_stream, @state, :confirmation_cancelled, pending)
108
+ { "status" => "cancelled", "tool" => pending.tool }
109
+ end
110
+ end
111
+ end
112
+ end
@@ -12,7 +12,7 @@ module Insika
12
12
  # it is NOT required in lib/insika.rb; the overlay loads it lazily at registration
13
13
  #
14
14
  # Contract preserved by duck-typing: it overrides name/description/parameters/
15
- # execute; RubyLLM's params_schema derives from #parameters automatically.
15
+ # execute; RubyLLM's parameters_schema derives from #parameters automatically.
16
16
  # execute NEVER raises — an error (missing param, blocked egress, HTTP, parse)
17
17
  # becomes `{ error: }` to the model, like the other tools.
18
18
  class DataDefinedTool < RubyLLM::Tool
@@ -47,21 +47,13 @@ module Insika
47
47
  # envelope's duck-typed resolution checks this FIRST — a data-tool declares
48
48
  # its evidence on its definition, never in the registry metadata.
49
49
  def evidence = @definition.evidence
50
+ def requires_evidence = @definition.requires_evidence
50
51
 
51
- # FULL (nested) JSON Schema straight into RubyLLM's params_schema — it is what
52
+ # FULL (nested) JSON Schema straight into RubyLLM's parameters_schema — it is what
52
53
  # the providers serialize (OpenAI/Anthropic/Gemini/Bedrock prefer
53
- # params_schema; parameters is just a fallback). Provider-agnostic and
54
+ # parameters_schema; parameters is just a fallback). Provider-agnostic and
54
55
  # the only form that expresses nesting (object/array/enum).,.
55
- def params_schema = @definition.parameters
56
-
57
- # FLAT top-level view for discovery (tool_search calls #parameters on the resolved
58
- # tool). The real nested schema goes through #params_schema above.
59
- def parameters
60
- @parameters ||= @definition.top_level_params.each_with_object({}) do |p, acc|
61
- sym = p[:name].to_sym
62
- acc[sym] = RubyLLM::Parameter.new(sym, type: p[:type], desc: p[:description], required: p[:required])
63
- end
64
- end
56
+ def parameters_schema = @definition.parameters
65
57
 
66
58
  # The args are checked against the tool's own JSON Schema BEFORE the request is
67
59
  # built: a call the schema does not allow becomes `{ error: }` the model can act
@@ -71,16 +63,19 @@ module Insika
71
63
  return { error: bad }
72
64
  end
73
65
 
66
+ t0 = monotonic
74
67
  req = build_request(kwargs)
75
68
  reason = @egress.violation(req[:url], **@egress_options)
76
69
  return { error: "destination blocked: #{reason}" } if reason
77
70
 
71
+ t1 = monotonic
78
72
  result = @http.request(**req)
73
+ record_timing(t0, t1, result)
79
74
  emit(result[:status])
80
75
  payload = extract(result)
81
76
  # The RESPONSE says the turn is over (`halt_when`): the backend already
82
77
  # answered the customer, so letting the model comment would deliver the
83
- # message twice. RubyLLM's Tool::Halt ends its loop right here — no second
78
+ # message twice. Insika::ToolDefinition::Halt ends its loop right here — no second
84
79
  # provider call, and the decision is the engine's, not a request in a prompt.
85
80
  # Only on a 2xx: an error body that happens to carry the value is a failure,
86
81
  # and a failure must reach the model.
@@ -89,7 +84,7 @@ module Insika
89
84
  # the model wrote no lead-in. Wrapped only when there is one, so every tool
90
85
  # that declares no `say` keeps producing exactly the payload it always did.
91
86
  say = @definition.halt_say(result[:body])
92
- return RubyLLM::Tool::Halt.new(say ? Insika::ToolDefinition.wrap_halt(payload, say) : payload)
87
+ return Insika::ToolDefinition::Halt.new(say ? Insika::ToolDefinition.wrap_halt(payload, say) : payload)
93
88
  end
94
89
 
95
90
  payload
@@ -219,6 +214,20 @@ module Insika
219
214
 
220
215
  # No task correlation (registry tool does not receive TurnState) -> meta {}.
221
216
  # Emits only name + status: NEVER body/headers (0 secret leakage, R2).
217
+ def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
218
+
219
+ # Where the call's time went, for the ToolEnvelope's trace (same fiber): the
220
+ # request build + egress check, the HTTP round trip as this process saw it, and
221
+ # the server's own runtime when it reports one. A wide gap between http_ms and
222
+ # server_ms is network or this process being slow to resume the call.
223
+ def record_timing(t0, t1, result)
224
+ Thread.current[Insika::ToolEnvelope::TIMING_KEY] = {
225
+ "pre_ms" => ((t1 - t0) * 1000).round,
226
+ "http_ms" => ((monotonic - t1) * 1000).round,
227
+ "server_ms" => result[:server_ms]
228
+ }.compact
229
+ end
230
+
222
231
  def emit(status)
223
232
  @event_stream&.emit(Insika::Event.new(
224
233
  type: :data_tool_call, data: { tool: @definition.name, status: status }, meta: {}
@@ -27,9 +27,9 @@ module Insika
27
27
  "prompt alone — UNLESS this turn carries an inbound photo, in which case " \
28
28
  "that photo is edited by default (pass source_image_urls explicitly to " \
29
29
  "generate from scratch instead)."
30
- # explicit JSON-schema form (the `param` DSL only reaches strings/scalars,
30
+ # explicit JSON-schema form (the `parameter` DSL only reaches strings/scalars,
31
31
  # and source_image_urls needs a typed array — the bare-array gotcha, #128).
32
- params(
32
+ parameters(
33
33
  type: "object",
34
34
  properties: {
35
35
  prompt: { type: "string", description: "What to draw, or what edit to make, in detail" },
@@ -16,7 +16,7 @@ module Insika
16
16
  # loads it lazily inside create_chat, same as load_skill.
17
17
  class LoadKnowledge < RubyLLM::Tool
18
18
  description "Loads the complete content of a learned concept by name"
19
- param :name, desc: "Exact concept name, as listed in <knowledge>"
19
+ parameter :name, description: "Exact concept name, as listed in <knowledge>"
20
20
 
21
21
  # RubyLLM::Tool#name derives from self.class.name — for a nested class it
22
22
  # produces "insika--tools--load_knowledge", not "load_knowledge" (which
@@ -47,7 +47,11 @@ module Insika
47
47
  private
48
48
 
49
49
  def load(name)
50
- content = @store.get(@agent_id, name.to_s, tenant: @tenant)
50
+ content = if @state&.respond_to?(:shared_conversations) && @state.shared_conversations
51
+ @state.shared_conversations.memory_get(task:@state.task,id:name.to_s,kind:'knowledge')&.fetch('value')
52
+ else
53
+ @store.get(@agent_id, name.to_s, tenant: @tenant)
54
+ end
51
55
  return { error: "concept '#{name}' not found" } unless content
52
56
 
53
57
  content
@@ -61,7 +65,7 @@ module Insika
61
65
 
62
66
  @trace_recorder.record(
63
67
  session_id: @state.task.session_id,
64
- entry: { "turn" => @state.turn, "tool" => "load_knowledge", "call_id" => "",
68
+ entry: { "task_id" => @state.task.id.to_s, "turn" => @state.turn, "tool" => "load_knowledge", "call_id" => "",
65
69
  "args" => { "name" => name.to_s }, "result" => result,
66
70
  "ms" => ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round,
67
71
  "at" => Time.now.utc.iso8601 }
@@ -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 }
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_llm"
4
+
5
+ module Insika
6
+ module Tools
7
+ # PRESENTATION tool: the model says WHICH products to show, the engine decides
8
+ # WHAT is shown. Built by the overlay registry from a ToolDefinition that
9
+ # declares `presentation` (one class, N instances — like DataDefinedTool).
10
+ #
11
+ # Cards used to be a side effect of evidence: every attachment a search tool
12
+ # returned rode the channel delivery, all of them, and the model had no way to
13
+ # say "these three, not the ten I searched". The deployments compensated with
14
+ # inline markers in the text, parsed by regex on their side — where most of
15
+ # the hallucinated ids came from. Here text never carries product data: the
16
+ # model picks ids, and the engine
17
+ # 1. keeps only ids the session's evidence ledger has seen (provenance),
18
+ # 2. joins each to the card the evidence tool already hoarded this turn,
19
+ # 3. caps at the declared `max`,
20
+ # 4. records the selection on the turn (the delivery sends THAT),
21
+ # 5. emits `:ui` on the stream (the edge publishes it as `insika.ui`),
22
+ # 6. tells the model what was shown and what was dropped, and why.
23
+ # No HTTP, no model call, deterministic. `execute` never raises.
24
+ class Present < RubyLLM::Tool
25
+ # The one sentence for the model when NOTHING could be shown. Engine-generic:
26
+ # the pack tunes vocabulary through the tool's `description`.
27
+ NOTHING_SHOWN = "no card could be shown; name the products in text or search again"
28
+
29
+ def initialize(definition:, event_stream: nil)
30
+ @definition = definition
31
+ @event_stream = event_stream
32
+ @state = nil
33
+ super()
34
+ end
35
+
36
+ # Deposited by ToolAssembly before the envelope wraps the instance — the
37
+ # ledger and the hoarded cards live on the turn, never on the registry.
38
+ def turn_state=(state)
39
+ @state = state
40
+ end
41
+
42
+ def name = @definition.name
43
+ def description = @definition.description
44
+ def parameters_schema = @definition.parameters
45
+
46
+ def execute(**kwargs)
47
+ spec = @definition.presentation
48
+ ids = Array(kwargs[spec[:ids].to_sym]).map(&:to_s).reject(&:empty?).uniq
49
+ title = Coercion.presence(kwargs[:title])
50
+
51
+ shown, dropped = select(ids, spec[:max])
52
+ record(spec[:component], title, shown)
53
+ emit(spec[:component], title, shown, dropped)
54
+
55
+ out = { "shown" => shown.map { |c| c["id"] }, "dropped" => dropped }
56
+ out["instruction"] = NOTHING_SHOWN if shown.empty?
57
+ out
58
+ rescue StandardError => e
59
+ { error: "presentation failed: #{e.message}" }
60
+ end
61
+
62
+ private
63
+
64
+ # -> [shown cards, dropped [{id, reason}]]. Order preserved. Reasons:
65
+ # unknown — the ledger never saw the id (the model made it up, or the
66
+ # customer typed it); no ledger on the state reads as unknown too —
67
+ # a check that cannot verify does not pass.
68
+ # no_card — the id is known but no evidence tool returned a card for it
69
+ # this session (a text-only search result, or a seed of ids alone).
70
+ # max — beyond the declared cap.
71
+ # Cards come from this turn first, then from the ledger (the last few
72
+ # searches of the session) — "show me the second one" a turn later works.
73
+ def select(ids, max)
74
+ ledger = @state.respond_to?(:evidence_ledger) ? @state.evidence_ledger : nil
75
+ known = Array(ledger&.ids)
76
+ turn_cards = @state.respond_to?(:evidence_attachments) ? Array(@state.evidence_attachments) : []
77
+ session_cards = ledger.respond_to?(:cards) ? Array(ledger.cards) : []
78
+ by_id = (turn_cards + session_cards).each_with_object({}) { |c, h| h[c["id"]] ||= c if c["id"] }
79
+
80
+ shown = []
81
+ dropped = []
82
+ ids.each do |id|
83
+ if !known.include?(id) then dropped << { "id" => id, "reason" => "unknown" }
84
+ elsif (card = by_id[id]).nil? then dropped << { "id" => id, "reason" => "no_card" }
85
+ elsif shown.size >= max then dropped << { "id" => id, "reason" => "max" }
86
+ else shown << card
87
+ end
88
+ end
89
+ [shown, dropped]
90
+ end
91
+
92
+ def record(component, title, shown)
93
+ return unless @state.respond_to?(:presentations)
94
+
95
+ @state.presentations ||= []
96
+ @state.presentations << { "component" => component, "title" => title,
97
+ "items" => shown.map { |c| c.slice("id", "type", "url", "caption") } }
98
+ end
99
+
100
+ def emit(component, title, shown, dropped)
101
+ return unless @event_stream
102
+
103
+ task = @state.respond_to?(:task) ? @state.task : nil
104
+ meta = task ? { task_id: task.id, session_id: task.session_id } : {}
105
+ @event_stream.emit(Insika::Event.new(
106
+ type: :ui,
107
+ data: { component: component, title: title,
108
+ items: shown.map { |c| c.slice("id", "type", "url", "caption") },
109
+ count: shown.size, dropped: dropped },
110
+ meta: meta
111
+ ))
112
+ end
113
+ end
114
+ end
115
+ end
@@ -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
 
@@ -161,7 +161,7 @@ module Insika
161
161
  simulator = Insika::Evals::Simulator.new(transport: transport, ask: persona_ask, safety: safety)
162
162
 
163
163
  conv = "eval-#{golden.id}-#{SecureRandom.hex(4)}" # a fresh session every run (never reused)
164
- run = simulator.run(persona: golden.persona, agent: golden.agent, conv: conv)
164
+ run = simulator.run(persona: golden.persona, agent: golden.agent, conv: conv, state: golden.state)
165
165
  verdict = judge.score_conversation(
166
166
  rubric: golden.rubric, transcript: run.transcript, policy: golden.policy,
167
167
  min_score: golden.min_score || Insika::Evals::Judge::DEFAULT_MIN_SCORE
@@ -199,6 +199,11 @@ module Insika
199
199
  llm: llm_context
200
200
  )
201
201
  bus = Insika::CommandBus.new
202
+ # a golden with `state:` seeds through the same bus the turn runs on
203
+ bus.register(:seed_session, Insika::Commands::SeedSession.new(
204
+ session_store: @graph.session_store, memory_store: @graph.memory_store,
205
+ event_stream: @graph.event_stream
206
+ ))
202
207
  bus.register(:send_message, Insika::Commands::SendMessage.new(
203
208
  profiles: @graph.profiles, session_store: @graph.session_store,
204
209
  task_store: @graph.task_store, executor: executor,
@@ -314,9 +319,7 @@ module Insika
314
319
  return if @budget_ledger.nil? || meter.empty?
315
320
 
316
321
  tokens = meter.sum do |m|
317
- m.input_tokens.to_i + m.output_tokens.to_i +
318
- (m.respond_to?(:cached_tokens) ? m.cached_tokens.to_i : 0) +
319
- (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
320
323
  end
321
324
  return if tokens.zero?
322
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