insika 0.2.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +325 -17
  3. data/README.md +52 -11
  4. data/bin/insika +895 -8
  5. data/bin/insika-router +87 -0
  6. data/docs/AGENTS.md +229 -163
  7. data/docs/API.md +56 -0
  8. data/docs/ARCHITECTURE.md +3 -2
  9. data/docs/ARTIFACTS.md +95 -0
  10. data/docs/BENCHMARK.md +2 -2
  11. data/docs/CHANNELS.md +111 -21
  12. data/docs/CONTEXT.md +132 -19
  13. data/docs/DEMO.md +80 -0
  14. data/docs/DEPLOY.md +75 -4
  15. data/docs/EMBEDDING.md +1 -1
  16. data/docs/EVALS.md +146 -2
  17. data/docs/FACTS.md +135 -0
  18. data/docs/HARVEST.md +116 -0
  19. data/docs/KNOWLEDGE.md +290 -0
  20. data/docs/LOADTEST.md +16 -10
  21. data/docs/MEDIA.md +128 -0
  22. data/docs/OBSERVABILITY.md +65 -6
  23. data/docs/OUTCOMES.md +137 -0
  24. data/docs/PLUGINS.md +51 -6
  25. data/docs/POLICY.md +216 -0
  26. data/docs/REFINEMENT.md +17 -12
  27. data/docs/RELEASING.md +27 -0
  28. data/docs/ROUTER.md +213 -0
  29. data/docs/RUNNING-LOCAL.md +6 -6
  30. data/docs/SCHEDULING.md +121 -0
  31. data/docs/SECURITY.md +79 -2
  32. data/docs/SKILLS.md +13 -4
  33. data/docs/SOAK.md +127 -0
  34. data/docs/TEMPLATES.md +134 -0
  35. data/docs/TOOLS.md +220 -27
  36. data/docs/WHY.md +1 -1
  37. data/docs/WORKFLOWS.md +2 -2
  38. data/docs/_includes/head_custom.html +5 -0
  39. data/docs/_includes/title.html +13 -0
  40. data/docs/_sass/color_schemes/insika.scss +32 -0
  41. data/docs/_sass/custom/custom.scss +199 -0
  42. data/docs/_sass/custom/setup.scss +26 -0
  43. data/docs/assets/img/favicon.svg +7 -0
  44. data/docs/assets/img/insika-mark.svg +7 -0
  45. data/docs/core-concepts.md +21 -0
  46. data/docs/domain.md +115 -0
  47. data/docs/improve.md +20 -0
  48. data/docs/index.md +9 -6
  49. data/docs/integrate.md +20 -0
  50. data/docs/operate.md +13 -6
  51. data/docs/prompts/ADD-TOOL.md +118 -0
  52. data/docs/prompts/DIAGNOSE-TURN.md +65 -0
  53. data/docs/prompts/GO-LIVE.md +138 -0
  54. data/docs/prompts/RUN-EXAMPLES.md +70 -0
  55. data/docs/reference.md +19 -0
  56. data/docs/ship.md +10 -2
  57. data/docs/start-here.md +18 -0
  58. data/lib/insika/agent_profile.rb +202 -10
  59. data/lib/insika/artifact_signing.rb +82 -0
  60. data/lib/insika/artifact_store.rb +160 -0
  61. data/lib/insika/balloon_splitter.rb +102 -0
  62. data/lib/insika/budget_ledger.rb +34 -3
  63. data/lib/insika/cache_series_store.rb +49 -0
  64. data/lib/insika/channel_delivery.rb +119 -14
  65. data/lib/insika/channels/relay.rb +77 -3
  66. data/lib/insika/channels/web.rb +2 -2
  67. data/lib/insika/chat_builder.rb +105 -2
  68. data/lib/insika/checkpoint_store.rb +16 -0
  69. data/lib/insika/commands/agent_payload.rb +3 -3
  70. data/lib/insika/commands/backfill_knowledge.rb +145 -0
  71. data/lib/insika/commands/cancel_followup.rb +49 -0
  72. data/lib/insika/commands/delete_artifact.rb +35 -0
  73. data/lib/insika/commands/delete_concept.rb +34 -0
  74. data/lib/insika/commands/delete_mcp.rb +6 -2
  75. data/lib/insika/commands/delete_tenant_data.rb +107 -0
  76. data/lib/insika/commands/export_customer_memory.rb +48 -0
  77. data/lib/insika/commands/forget_customer.rb +117 -0
  78. data/lib/insika/commands/freeze_funnel_baseline.rb +113 -0
  79. data/lib/insika/commands/gate_harvest.rb +138 -0
  80. data/lib/insika/commands/gate_refinement.rb +1 -1
  81. data/lib/insika/commands/judge_shadow_pairs.rb +124 -0
  82. data/lib/insika/commands/memory_forget_fact.rb +20 -4
  83. data/lib/insika/commands/memory_put_fact.rb +23 -4
  84. data/lib/insika/commands/promote_harvest.rb +130 -0
  85. data/lib/insika/commands/record_outcome.rb +46 -0
  86. data/lib/insika/commands/record_shadow_reply.rb +68 -0
  87. data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
  88. data/lib/insika/commands/reject_harvest.rb +38 -0
  89. data/lib/insika/commands/resolve_proposal.rb +108 -0
  90. data/lib/insika/commands/restore_concept.rb +34 -0
  91. data/lib/insika/commands/revoke_contact.rb +49 -0
  92. data/lib/insika/commands/rollback_harvest.rb +86 -0
  93. data/lib/insika/commands/run_distillation.rb +186 -0
  94. data/lib/insika/commands/run_harvest.rb +393 -0
  95. data/lib/insika/commands/seed_demo_data.rb +31 -0
  96. data/lib/insika/commands/send_message.rb +103 -6
  97. data/lib/insika/commands/session_purge.rb +67 -0
  98. data/lib/insika/commands/upsert_mcp.rb +6 -3
  99. data/lib/insika/commands/write_concept.rb +57 -0
  100. data/lib/insika/contact_store.rb +183 -0
  101. data/lib/insika/context/builder.rb +21 -3
  102. data/lib/insika/context/fragment.rb +7 -3
  103. data/lib/insika/context/priority.rb +5 -0
  104. data/lib/insika/context/provider.rb +17 -3
  105. data/lib/insika/context/providers/briefing.rb +96 -0
  106. data/lib/insika/context/providers/knowledge.rb +108 -0
  107. data/lib/insika/context/providers/memory.rb +16 -7
  108. data/lib/insika/context/providers/prompt.rb +57 -23
  109. data/lib/insika/context/providers/skill.rb +2 -0
  110. data/lib/insika/context/providers/tool_search.rb +2 -0
  111. data/lib/insika/context_trace_store.rb +38 -2
  112. data/lib/insika/cron.rb +189 -0
  113. data/lib/insika/demo/agent_attrs.rb +43 -0
  114. data/lib/insika/demo/golden_cases.rb +81 -0
  115. data/lib/insika/demo/seeder.rb +336 -0
  116. data/lib/insika/distill.rb +224 -0
  117. data/lib/insika/distill_engine.rb +169 -0
  118. data/lib/insika/doctor.rb +889 -11
  119. data/lib/insika/dsl/definition.rb +3 -2
  120. data/lib/insika/dsl/runtime.rb +69 -82
  121. data/lib/insika/dsl/server_boot.rb +92 -1
  122. data/lib/insika/dsl/system.rb +10 -2
  123. data/lib/insika/dsl.rb +186 -5
  124. data/lib/insika/edge_limiter.rb +28 -7
  125. data/lib/insika/env_schema.rb +25 -3
  126. data/lib/insika/errors.rb +11 -0
  127. data/lib/insika/evals/assertions.rb +3 -2
  128. data/lib/insika/evals/golden.rb +41 -4
  129. data/lib/insika/evals/judge.rb +47 -2
  130. data/lib/insika/evals/pairwise.rb +29 -6
  131. data/lib/insika/evals/persona.rb +98 -0
  132. data/lib/insika/evals/runner.rb +9 -0
  133. data/lib/insika/evals/simulator.rb +225 -0
  134. data/lib/insika/evals/transport.rb +83 -1
  135. data/lib/insika/event_stream.rb +10 -0
  136. data/lib/insika/evidence.rb +183 -0
  137. data/lib/insika/executor.rb +869 -76
  138. data/lib/insika/followup_engine.rb +207 -0
  139. data/lib/insika/followup_policy.rb +198 -0
  140. data/lib/insika/followup_store.rb +306 -0
  141. data/lib/insika/funnel_declaration.rb +106 -0
  142. data/lib/insika/funnel_fold.rb +179 -0
  143. data/lib/insika/funnel_store.rb +163 -0
  144. data/lib/insika/golden_store.rb +17 -2
  145. data/lib/insika/grounding/matcher.rb +69 -0
  146. data/lib/insika/grounding.rb +44 -0
  147. data/lib/insika/harvest/conversion_gate.rb +159 -0
  148. data/lib/insika/harvest/criterion.rb +98 -0
  149. data/lib/insika/harvest/gate.rb +194 -0
  150. data/lib/insika/harvest/negative_list.rb +199 -0
  151. data/lib/insika/harvest.rb +241 -0
  152. data/lib/insika/harvest_engine.rb +193 -0
  153. data/lib/insika/harvest_store.rb +548 -0
  154. data/lib/insika/knowledge.rb +680 -0
  155. data/lib/insika/knowledge_store.rb +140 -0
  156. data/lib/insika/mcp_client.rb +94 -0
  157. data/lib/insika/mcp_json.rb +74 -0
  158. data/lib/insika/mcp_live_tool.rb +43 -0
  159. data/lib/insika/mcp_store.rb +98 -26
  160. data/lib/insika/mcp_tool_ingestor.rb +30 -8
  161. data/lib/insika/mcp_tool_registry.rb +100 -0
  162. data/lib/insika/media.rb +382 -0
  163. data/lib/insika/memory_audit_store.rb +85 -0
  164. data/lib/insika/memory_store.rb +264 -23
  165. data/lib/insika/message_origin.rb +7 -2
  166. data/lib/insika/middleware.rb +9 -0
  167. data/lib/insika/model_visible.rb +87 -0
  168. data/lib/insika/model_visible_trace_store.rb +66 -0
  169. data/lib/insika/onboarding.rb +22 -1
  170. data/lib/insika/outbox_store.rb +42 -4
  171. data/lib/insika/outcome_store.rb +147 -0
  172. data/lib/insika/overlay_tool_registry.rb +37 -17
  173. data/lib/insika/packaging.rb +163 -0
  174. data/lib/insika/parity/criterion.rb +79 -0
  175. data/lib/insika/parity/verdict.rb +318 -0
  176. data/lib/insika/prefix_fingerprint.rb +58 -0
  177. data/lib/insika/profile_source.rb +29 -1
  178. data/lib/insika/prompt_catalog.rb +10 -0
  179. data/lib/insika/proposal_store.rb +271 -0
  180. data/lib/insika/queue_policy.rb +4 -1
  181. data/lib/insika/refinement/proposer.rb +1 -1
  182. data/lib/insika/reliability.rb +32 -6
  183. data/lib/insika/retention.rb +316 -0
  184. data/lib/insika/router/app.rb +157 -0
  185. data/lib/insika/router/backend_pool.rb +98 -0
  186. data/lib/insika/router/hash_ring.rb +55 -0
  187. data/lib/insika/router/proxy_body.rb +34 -0
  188. data/lib/insika/router/session_key.rb +54 -0
  189. data/lib/insika/router.rb +18 -0
  190. data/lib/insika/routing.rb +101 -0
  191. data/lib/insika/safety/config.rb +44 -4
  192. data/lib/insika/safety/corpus.rb +255 -0
  193. data/lib/insika/safety/detectors.rb +32 -113
  194. data/lib/insika/safety/factory.rb +15 -2
  195. data/lib/insika/safety/grounding_enforcer.rb +59 -0
  196. data/lib/insika/safety/grounding_validator.rb +49 -0
  197. data/lib/insika/safety/input_guardrail.rb +1 -1
  198. data/lib/insika/safety/moderator.rb +1 -1
  199. data/lib/insika/safety/output_filter.rb +9 -5
  200. data/lib/insika/safety/output_validator.rb +11 -5
  201. data/lib/insika/schedule.rb +177 -0
  202. data/lib/insika/schedule_engine.rb +314 -0
  203. data/lib/insika/schedule_store.rb +208 -0
  204. data/lib/insika/schema_guard.rb +35 -0
  205. data/lib/insika/server/app.rb +247 -17
  206. data/lib/insika/server/rack_app.rb +21 -1
  207. data/lib/insika/server/responses.rb +40 -1
  208. data/lib/insika/session_actor.rb +8 -4
  209. data/lib/insika/session_store.rb +65 -2
  210. data/lib/insika/settings_store.rb +10 -0
  211. data/lib/insika/shadow_pair_store.rb +258 -0
  212. data/lib/insika/skill_catalog.rb +16 -0
  213. data/lib/insika/soak/envelope.rb +140 -0
  214. data/lib/insika/soak/report.rb +392 -0
  215. data/lib/insika/soak/runner.rb +554 -0
  216. data/lib/insika/steer_injector.rb +21 -10
  217. data/lib/insika/store.rb +10 -1
  218. data/lib/insika/stores/memory.rb +6 -0
  219. data/lib/insika/stores/sqlite.rb +8 -0
  220. data/lib/insika/studio/app.rb +1437 -54
  221. data/lib/insika/studio/assets/dist/application.css +1 -1
  222. data/lib/insika/studio/assets/dist/application.js +23 -23
  223. data/lib/insika/studio/assets/dist/favicon.svg +6 -0
  224. data/lib/insika/studio/forms.rb +308 -10
  225. data/lib/insika/studio/nav_icons.rb +19 -1
  226. data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
  227. data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
  228. data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
  229. data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
  230. data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
  231. data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
  232. data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
  233. data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
  234. data/lib/insika/studio/views/_agents_master.erb +44 -0
  235. data/lib/insika/studio/views/_message.erb +49 -32
  236. data/lib/insika/studio/views/agent_detail.erb +62 -278
  237. data/lib/insika/studio/views/agents.erb +71 -54
  238. data/lib/insika/studio/views/approvals.erb +4 -1
  239. data/lib/insika/studio/views/artifact.erb +23 -0
  240. data/lib/insika/studio/views/artifacts.erb +59 -0
  241. data/lib/insika/studio/views/chats.erb +4 -1
  242. data/lib/insika/studio/views/customer.erb +94 -0
  243. data/lib/insika/studio/views/customers.erb +32 -0
  244. data/lib/insika/studio/views/evals.erb +6 -3
  245. data/lib/insika/studio/views/facts.erb +133 -0
  246. data/lib/insika/studio/views/followups.erb +125 -0
  247. data/lib/insika/studio/views/funnel.erb +106 -0
  248. data/lib/insika/studio/views/harvest.erb +234 -0
  249. data/lib/insika/studio/views/home.erb +106 -66
  250. data/lib/insika/studio/views/knowledge.erb +123 -0
  251. data/lib/insika/studio/views/layout.erb +15 -11
  252. data/lib/insika/studio/views/mcp.erb +174 -80
  253. data/lib/insika/studio/views/parity.erb +147 -0
  254. data/lib/insika/studio/views/playground.erb +7 -1
  255. data/lib/insika/studio/views/session.erb +233 -124
  256. data/lib/insika/studio/views/settings.erb +41 -1
  257. data/lib/insika/studio/views/skills.erb +1 -2
  258. data/lib/insika/studio/views/system_files.erb +1 -1
  259. data/lib/insika/studio/views/task.erb +13 -0
  260. data/lib/insika/studio/views/tasks.erb +4 -1
  261. data/lib/insika/studio/views/tools.erb +24 -10
  262. data/lib/insika/task_store.rb +21 -1
  263. data/lib/insika/templates/browser-agent/README.md +36 -0
  264. data/lib/insika/templates/browser-agent/agent.rb +49 -0
  265. data/lib/insika/templates/daily-digest/README.md +38 -0
  266. data/lib/insika/templates/daily-digest/agent.rb +77 -0
  267. data/lib/insika/templates/repo-explorer/README.md +36 -0
  268. data/lib/insika/templates/repo-explorer/agent.rb +45 -0
  269. data/lib/insika/templates/research-analyst/README.md +26 -0
  270. data/lib/insika/templates/research-analyst/agent.rb +58 -0
  271. data/lib/insika/templates/review-panel/README.md +20 -0
  272. data/lib/insika/templates/review-panel/agent.rb +50 -0
  273. data/lib/insika/templates/travel-planner/README.md +35 -0
  274. data/lib/insika/templates/travel-planner/agent.rb +87 -0
  275. data/lib/insika/templates.rb +112 -0
  276. data/lib/insika/testing/store_contract.rb +27 -6
  277. data/lib/insika/tick.rb +47 -11
  278. data/lib/insika/timezone.rb +45 -0
  279. data/lib/insika/tool_definition.rb +17 -7
  280. data/lib/insika/tool_envelope.rb +69 -0
  281. data/lib/insika/tool_manifest.rb +5 -1
  282. data/lib/insika/tools/data_defined_tool.rb +10 -0
  283. data/lib/insika/tools/generate_image.rb +89 -0
  284. data/lib/insika/tools/load_knowledge.rb +74 -0
  285. data/lib/insika/tools/run_persona_eval.rb +328 -0
  286. data/lib/insika/tools/save_artifact.rb +95 -0
  287. data/lib/insika/tools/schedule_followup.rb +164 -0
  288. data/lib/insika/tools/tts.rb +47 -0
  289. data/lib/insika/tools/update_briefing.rb +126 -0
  290. data/lib/insika/turn_output.rb +1 -1
  291. data/lib/insika/turn_state.rb +49 -1
  292. data/lib/insika/turn_timing.rb +22 -2
  293. data/lib/insika/version.rb +1 -1
  294. data/lib/insika/vitals.rb +84 -0
  295. data/lib/insika/wiring/graph.rb +472 -13
  296. data/lib/insika/wiring/graph_chat.rb +102 -0
  297. data/lib/insika.rb +149 -3
  298. metadata +181 -5
  299. data/docs/build.md +0 -14
  300. data/docs/understand.md +0 -10
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_llm"
4
+
5
+ module Insika
6
+ module Tools
7
+ # The agent's SPEECH output (WS9, saída). The engine transports media, never
8
+ # meaning: the words belong to the customer's channel (a voice note on
9
+ # WhatsApp), and the contract here just produces the audio bytes and carries
10
+ # them in the turn's `output_parts`.
11
+ #
12
+ # Wired ONLY when both gates pass (ChatBuilder): the agent opted in
13
+ # (`outputs.tts`) AND the channel declared it can receive the media
14
+ # (`channel.capabilities` includes "audio_output") — nothing leaks by
15
+ # default. The clip is an envelope part, never part of the answer text;
16
+ # the turn counts the call in its usage (`usage.media`) — the provider's
17
+ # speech API reports no token counts, so the part carries the model for
18
+ # consumer-side pricing.
19
+ class Tts < RubyLLM::Tool
20
+ description "Synthesize speech for the reply — a voice clip travels as an " \
21
+ "output part in the envelope, the channel supports it. Use when the " \
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)",
25
+ required: false
26
+
27
+ def name = "tts"
28
+
29
+ # runner: a duck exposing #generate_media_output(:tts, text, config)
30
+ # -> [part, usage] and #account_media_usage(part, usage) (the Executor).
31
+ def initialize(runner:, config:, state:, **)
32
+ @runner = runner
33
+ @config = config
34
+ @state = state
35
+ super()
36
+ end
37
+
38
+ def execute(text:, voice: nil)
39
+ cfg = @config.merge("voice" => voice.to_s).reject { |_, v| v.to_s.empty? }
40
+ part, usage = @runner.generate_media_output(:tts, text.to_s, cfg)
41
+ @state.output_parts << part
42
+ @runner.account_media_usage(@state, part, usage)
43
+ "speech synthesized and attached to the reply (#{part["mime_type"]})"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_llm"
4
+ require_relative "agent_enum"
5
+
6
+ module Insika
7
+ module Tools
8
+ # Shared write-edge plumbing for the two briefing tools: the
9
+ # session id always comes from the turn's task — never from the model's
10
+ # arguments — and the :briefing_updated event carries task/session
11
+ # correlation for the trace.
12
+ module BriefingWrite
13
+ private
14
+
15
+ # The turn's session, from the task (turn_state.rb:7) — never the model's args.
16
+ def session_id
17
+ @state.respond_to?(:task) ? @state.task&.session_id : nil
18
+ end
19
+
20
+ def no_session_error = { error: "no session to brief" }
21
+
22
+ def emit(kind, field, value)
23
+ @event_stream&.emit(Insika::Event.new(
24
+ type: :briefing_updated,
25
+ data: { kind: kind, field: field, value: value },
26
+ meta: { task_id: @state.task&.id, session_id: @state.task&.session_id }
27
+ ))
28
+ end
29
+ end
30
+
31
+ # Write edge of the session briefing: the agent records what it
32
+ # learned in THIS conversation — the working state it is accountable for
33
+ # (tool-written, never engine-inferred). System builtin (like remember):
34
+ # `require "ruby_llm"` stays in THIS file, loaded lazily by the Executor in
35
+ # create_chat. Wired only when the pack declared briefing_fields AND the
36
+ # builder has a session_store (double gate, like remember). Never enveloped:
37
+ # deterministic in-process writes.
38
+ class UpdateBriefing < RubyLLM::Tool
39
+ include BriefingWrite
40
+
41
+ # The class-level text carries no field list, so it can never render the
42
+ # empty "field must be one of: ." placeholder; the instance adds the
43
+ # DECLARED names (D3) to the description AND as the `field` enum (the
44
+ # model sees the valid set in the schema instead of inventing one —
45
+ # the set is instance-built. Per-instance, like the subagent tools' dynamic schemas.
46
+ BASE_DESCRIPTION = "Records a fact learned in this conversation into the session briefing. " \
47
+ "Call it as soon as the customer gives one of these, and when they correct one."
48
+
49
+ description BASE_DESCRIPTION
50
+
51
+ def self.description_for(fields)
52
+ names = Array(fields).map(&:to_s)
53
+ names.empty? ? BASE_DESCRIPTION : "#{BASE_DESCRIPTION} field must be one of: #{names.join(', ')}."
54
+ end
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."
58
+
59
+ def name = "update_briefing"
60
+
61
+ def initialize(session_store:, fields:, event_stream:, state:)
62
+ @session_store = session_store
63
+ @fields = Array(fields).map(&:to_s)
64
+ @event_stream = event_stream
65
+ @state = state
66
+ super()
67
+ end
68
+
69
+ def description
70
+ self.class.description_for(@fields)
71
+ end
72
+
73
+ # The declared names become the `field` enum in the schema the provider
74
+ # sees — per-turn data, like the subagent allowlist (Tools::AgentEnum).
75
+ def params_schema
76
+ @field_enum_schema ||= Insika::Tools::AgentEnum.inject(super, @fields, path: %i[field])
77
+ end
78
+
79
+ # E2: an undeclared name returns an ENVELOPE error to the model and never
80
+ # persists. A missing session is the same shape (the model can retry later).
81
+ def execute(field:, value:)
82
+ sid = session_id
83
+ return no_session_error if sid.to_s.empty?
84
+ unless @fields.include?(field.to_s)
85
+ return { error: "unknown field '#{field}'; declared: #{@fields.join(', ')}" }
86
+ end
87
+
88
+ session = @session_store.update_briefing(sid, field: field.to_s, value: value.to_s)
89
+ emit("field", field.to_s, value.to_s)
90
+ { updated: field.to_s, briefing: session.briefing }
91
+ rescue Insika::NotFoundError
92
+ { error: "session not found" }
93
+ end
94
+
95
+ # The agreed next step: same engine-owned briefing object,
96
+ # sibling writer. Blank clears to nil.
97
+ class SetNextStep < RubyLLM::Tool
98
+ include BriefingWrite
99
+
100
+ description "Records the next step agreed with the customer " \
101
+ "(e.g. 'send the payment link tomorrow at 10'). Blank clears it."
102
+ param :text, desc: "The agreed next step, in one sentence"
103
+
104
+ def name = "set_next_step"
105
+
106
+ def initialize(session_store:, event_stream:, state:)
107
+ @session_store = session_store
108
+ @event_stream = event_stream
109
+ @state = state
110
+ super()
111
+ end
112
+
113
+ def execute(text:)
114
+ sid = session_id
115
+ return no_session_error if sid.to_s.empty?
116
+
117
+ session = @session_store.set_next_step(sid, text: text.to_s)
118
+ emit("next_step", nil, text.to_s)
119
+ { next_step: session.briefing["next_step"] }
120
+ rescue Insika::NotFoundError
121
+ { error: "session not found" }
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -33,7 +33,7 @@ module Insika
33
33
  # answer; the fragment is still on the stream as `:intermediate` for whoever is
34
34
  # debugging it.
35
35
  #
36
- # `halt_when` (PR #130) is the one case where narration IS the turn: the tool
36
+ # `halt_when` is the one case where narration IS the turn: the tool
37
37
  # already answered the customer, and the model's lead-in ("vou te inscrever
38
38
  # agora") is all the turn is worth. That text is retained and published by the
39
39
  # Executor's halt branch — see #halt_text.
@@ -25,11 +25,50 @@ module Insika
25
25
  # (after_task); the Executor emits one :guardrail_flagged each.
26
26
  :response_content, # the turn's final assistant text, set at stage 6/on halt so the
27
27
  # after_task validator can inspect it.
28
+ :route, # WS4: the intent route the classifier chose (Symbol | nil).
29
+ # Set before the ask; rides the :route_classified event and the
30
+ # terminal event additively. nil = no routing on this turn.
31
+ :message_source, # WS9: how the turn's message entered (:voice when it came
32
+ # from a transcribed audio part; nil = typed). Rides the
33
+ # terminal event additively.
34
+ :media_attachments, # WS9: the ask's attachments (image + document parts);
35
+ # nil = none. The provider bills them; usage flows like
36
+ # any ask. The first URL of each kind also lands on
37
+ # turn_context (`{{ctx.image_url}}` / `{{ctx.document_url}}`).
38
+ :image_attachments, # WS9: the IMAGE subset of media_attachments (nil = no
39
+ # inbound image this turn). The default edit SOURCE for
40
+ # generate_image — kept separate from
41
+ # media_attachments because that one may also hold a
42
+ # document, which is never a paint(with:) source.
43
+ :output_parts, # WS9 (saída): media the turn GENERATED, as additive
44
+ # parts ({ type:, mime_type:, base64:, model: }). Rides
45
+ # the terminal event (output_parts) and the /v1/responses
46
+ # envelope; the answer text stays text on purpose — the
47
+ # media is a separate sibling. nil = nothing generated.
48
+ :channel_capabilities, # WS9 (saída): the CHANNEL's declared OUTPUT media
49
+ # kinds (Media::OUTPUT_CAPABILITIES — image_output /
50
+ # audio_output). Half of the gate that decides whether
51
+ # generate_image/tts are wired at all (the other half is
52
+ # profile.outputs). [] = the channel declared nothing —
53
+ # no media can leak to it.
28
54
  :output_filter, # per-turn Safety::OutputFilter (nil = off); redacts the stream.
29
- :stuck_outcome # set by the signal_stuck system tool (WS5):
55
+ :stuck_outcome, # set by the signal_stuck system tool (WS5):
30
56
  # { reason:, message: } when the agent declared it cannot
31
57
  # proceed. The Executor tags the terminal event with
32
58
  # outcome: "stuck" and emits :turn_stuck. nil = normal turn.
59
+ :evidence_ledger, # Insika::EvidenceLedger | nil — the
60
+ # session-scoped set of product ids that entered the
61
+ # context via an evidence-declared tool (the envelope
62
+ # appends; the validator/enforcer read it). Built per
63
+ # turn by the Executor; nil = no session/no evidence.
64
+ :evidence_attachments, # [ {type, url, caption} ] hoarded by the
65
+ # envelope this turn; read by the Executor at stage 8
66
+ # for the channel delivery. Reset per turn.
67
+ :context_trace_entry # the sanitized trace entry parked at
68
+ # prepare_turn (fingerprints + invalidation_reason);
69
+ # the stage-8 stamp merges the cache-hit fields into
70
+ # it and re-records the same (task_id, turn) key.
71
+ # nil = no store wired / no session.
33
72
 
34
73
  # Internal (not part of the contract): per-CALL correlation between RubyLLM's
35
74
  # tool callbacks and the tool decorators — `current_tool_call` keys the
@@ -80,6 +119,12 @@ module Insika
80
119
  # rules the run started under.
81
120
  attr_accessor :queue_policy
82
121
 
122
+ # Internal: the run's SteerInjector, or nil when steering is off (or the chat
123
+ # cannot host the boundary). Rebuilt per ATTEMPT — a WS3 fallback builds a new
124
+ # chat, and the injector belongs to the chat it appends to. The Executor reaches
125
+ # for it at the end of stage 6 to absorb whatever no tool boundary took.
126
+ attr_accessor :steer_injector
127
+
83
128
  # Internal: true when this turn re-enters the pipeline via
84
129
  # resume_task/recovery. The EdgeLimiter reads it to NEVER re-count or block a
85
130
  # turn that was already admitted — a crash/pause under a saturated window must
@@ -151,6 +196,9 @@ module Insika
151
196
  @turn = turn
152
197
  @message = message
153
198
  @capability_names = {}
199
+ @output_parts = []
200
+ @channel_capabilities = []
201
+ @evidence_attachments = []
154
202
  # Fiber storage is INHERITED by fibers created later, so a turn spawned from
155
203
  # inside a tool call (a subagent child) would start out carrying its
156
204
  # parent's correlation. Clearing at turn start keeps a child from keying its
@@ -26,22 +26,42 @@ module Insika
26
26
  Insika::EnvSchema.truthy?(Insika::EnvSchema.read("INSIKA_TURN_TIMING", env))
27
27
  end
28
28
 
29
- def initialize
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
34
+
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.
38
+ def initialize(breakdown: true)
30
39
  @marks = {}
40
+ @breakdown = breakdown
31
41
  end
32
42
 
33
43
  def mark(name)
44
+ return unless @breakdown || !BREAKDOWN_MARKS.include?(name)
45
+
34
46
  @marks[name] ||= Process.clock_gettime(Process::CLOCK_MONOTONIC)
35
47
  end
36
48
 
49
+ # Has this mark fired? Used by `SendMessage` to prove the channel clock was
50
+ # stamped at 202 acceptance (`:inbound` before the debounce window), and by
51
+ # the pipeline to know a threaded clock already started.
52
+ def marked?(name) = @marks.key?(name)
53
+
37
54
  # -> Hash of phase deltas in ms (only the windows whose endpoints both fired;
38
55
  # a workflow turn has no ask/first_token, so those are simply absent).
56
+ # first_balloon_ms is the inbound -> first outbox flush window.
57
+ # A missing number is never a zero — no mark, no key.
39
58
  def to_h
40
59
  {
41
60
  prep_ms: delta(:prep_start, :ask),
42
61
  ttft_ms: delta(:ask, :first_token),
43
62
  gen_ms: delta(:first_token, :done),
44
- total_ms: delta(:prep_start, :done)
63
+ total_ms: delta(:prep_start, :done),
64
+ first_balloon_ms: delta(:inbound, :first_balloon)
45
65
  }.compact
46
66
  end
47
67
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Insika
4
- VERSION = "0.2.0"
4
+ VERSION = "0.7.0"
5
5
  end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Insika
4
+ # Process vitals. One cheap read-only answer to "which process
5
+ # is this, how long has it been up, how much memory does it hold, and what is
6
+ # the Ruby heap doing?" It touches NO store, allocates nothing meaningful,
7
+ # and knows nothing about soaks — a general operator surface the soak
8
+ # happens to be the first consumer of. Exposed at GET /v1/vitals (operator
9
+ # only).
10
+ module Vitals
11
+ # Stamped when this file is first required: the process's own start, which
12
+ # is what "uptime" has to mean (Falcon's controller start is not this
13
+ # worker's). Per-process, not per-App, so a worker respawn under a live
14
+ # container keeps its real age and a new pid reads as a new clock.
15
+ STARTED_AT = Time.now.utc
16
+
17
+ # The subset of GC.stat that answers "is the Ruby heap growing, or is this
18
+ # the allocator?" — the difference between a leak and fragmentation, which
19
+ # is the first fork of any leak hunt .
20
+ GC_KEYS = %i[heap_live_slots heap_free_slots heap_allocated_pages
21
+ total_allocated_objects total_freed_objects
22
+ major_gc_count minor_gc_count
23
+ malloc_increase_bytes oldmalloc_increase_bytes].freeze
24
+
25
+ module_function
26
+
27
+ # -> Hash with STRING keys (it is a JSON body, not an internal value
28
+ # object). `executor:`/`db_path:` are optional — absent, the body simply
29
+ # omits those readings. `env:` is injected for specs.
30
+ def snapshot(executor: nil, db_path: nil, env: ENV)
31
+ {
32
+ "boot_id" => EnvSchema.read("INSIKA_BOOT_ID", env).to_s,
33
+ "pid" => Process.pid,
34
+ "started_at" => STARTED_AT.iso8601,
35
+ "uptime_s" => (Time.now.utc - STARTED_AT).round,
36
+ "version" => Insika::VERSION,
37
+ "ruby" => RUBY_DESCRIPTION,
38
+ "yjit" => (defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?),
39
+ "rss_bytes" => rss_bytes,
40
+ "gc" => gc_stat,
41
+ "threads" => Thread.list.size,
42
+ "in_flight" => executor&.in_flight&.size,
43
+ "db_bytes" => db_bytes(db_path),
44
+ "at" => Time.now.utc.iso8601
45
+ }
46
+ end
47
+
48
+ # Resident set size in bytes. Linux first (/proc/self/status VmRSS) —
49
+ # production is a Linux container; macOS/dev fall back to `ps`. Neither
50
+ # readable -> nil, NEVER a guess: a fabricated RSS would silently pass an
51
+ # envelope.
52
+ def rss_bytes
53
+ if File.file?("/proc/self/status")
54
+ match = File.read("/proc/self/status", encoding: "ASCII-8BIT")[/^VmRSS:\s+(\d+)\s*kB/, 1]
55
+ return Integer(match) * 1024 if match
56
+ end
57
+
58
+ out = `ps -o rss= -p #{Process.pid} 2>/dev/null`.strip
59
+ return Integer(out) * 1024 unless out.empty?
60
+
61
+ nil
62
+ rescue StandardError
63
+ nil
64
+ end
65
+
66
+ def gc_stat
67
+ stat = GC.stat
68
+ GC_KEYS.each_with_object({}) { |key, acc| acc[key.to_s] = stat[key] }
69
+ end
70
+
71
+ # SQLite file + WAL + shm bytes. -> { "db" =>, "wal" =>, "shm" => } | nil.
72
+ def db_bytes(path)
73
+ return nil if path.nil? || path.to_s.empty?
74
+
75
+ {
76
+ "db" => File.size(path.to_s),
77
+ "wal" => File.exist?("#{path}-wal") ? File.size("#{path}-wal") : 0,
78
+ "shm" => File.exist?("#{path}-shm") ? File.size("#{path}-shm") : 0
79
+ }
80
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR
81
+ nil
82
+ end
83
+ end
84
+ end