insika 0.2.0 → 0.3.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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +145 -17
  3. data/README.md +7 -1
  4. data/bin/insika +211 -8
  5. data/docs/AGENTS.md +377 -2
  6. data/docs/API.md +56 -0
  7. data/docs/CHANNELS.md +99 -9
  8. data/docs/CONTEXT.md +127 -16
  9. data/docs/DEPLOY.md +5 -2
  10. data/docs/EVALS.md +19 -0
  11. data/docs/FACTS.md +135 -0
  12. data/docs/HARVEST.md +117 -0
  13. data/docs/LOADTEST.md +14 -8
  14. data/docs/OBSERVABILITY.md +54 -0
  15. data/docs/REFINEMENT.md +3 -3
  16. data/docs/RELEASING.md +27 -0
  17. data/docs/RUNNING-LOCAL.md +3 -3
  18. data/docs/SECURITY.md +61 -0
  19. data/docs/SKILLS.md +2 -2
  20. data/docs/SOAK.md +127 -0
  21. data/docs/TOOLS.md +70 -2
  22. data/docs/domain.md +115 -0
  23. data/docs/index.md +1 -1
  24. data/lib/insika/agent_profile.rb +145 -10
  25. data/lib/insika/balloon_splitter.rb +102 -0
  26. data/lib/insika/budget_ledger.rb +34 -3
  27. data/lib/insika/cache_series_store.rb +49 -0
  28. data/lib/insika/channel_delivery.rb +118 -13
  29. data/lib/insika/channels/relay.rb +77 -3
  30. data/lib/insika/channels/web.rb +2 -2
  31. data/lib/insika/chat_builder.rb +84 -1
  32. data/lib/insika/checkpoint_store.rb +16 -0
  33. data/lib/insika/commands/agent_payload.rb +3 -3
  34. data/lib/insika/commands/cancel_followup.rb +49 -0
  35. data/lib/insika/commands/delete_tenant_data.rb +95 -0
  36. data/lib/insika/commands/export_customer_memory.rb +48 -0
  37. data/lib/insika/commands/forget_customer.rb +117 -0
  38. data/lib/insika/commands/freeze_funnel_baseline.rb +113 -0
  39. data/lib/insika/commands/gate_harvest.rb +138 -0
  40. data/lib/insika/commands/gate_refinement.rb +1 -1
  41. data/lib/insika/commands/judge_shadow_pairs.rb +124 -0
  42. data/lib/insika/commands/memory_forget_fact.rb +20 -4
  43. data/lib/insika/commands/memory_put_fact.rb +23 -4
  44. data/lib/insika/commands/promote_harvest.rb +130 -0
  45. data/lib/insika/commands/record_outcome.rb +46 -0
  46. data/lib/insika/commands/record_shadow_reply.rb +68 -0
  47. data/lib/insika/commands/reject_harvest.rb +38 -0
  48. data/lib/insika/commands/resolve_proposal.rb +108 -0
  49. data/lib/insika/commands/revoke_contact.rb +49 -0
  50. data/lib/insika/commands/rollback_harvest.rb +86 -0
  51. data/lib/insika/commands/run_distillation.rb +186 -0
  52. data/lib/insika/commands/run_harvest.rb +393 -0
  53. data/lib/insika/commands/send_message.rb +103 -6
  54. data/lib/insika/commands/session_purge.rb +67 -0
  55. data/lib/insika/contact_store.rb +183 -0
  56. data/lib/insika/context/builder.rb +21 -3
  57. data/lib/insika/context/fragment.rb +7 -3
  58. data/lib/insika/context/priority.rb +3 -0
  59. data/lib/insika/context/provider.rb +17 -3
  60. data/lib/insika/context/providers/briefing.rb +96 -0
  61. data/lib/insika/context/providers/memory.rb +16 -7
  62. data/lib/insika/context/providers/prompt.rb +30 -2
  63. data/lib/insika/context/providers/skill.rb +2 -0
  64. data/lib/insika/context/providers/tool_search.rb +2 -0
  65. data/lib/insika/context_trace_store.rb +38 -2
  66. data/lib/insika/distill.rb +224 -0
  67. data/lib/insika/distill_engine.rb +169 -0
  68. data/lib/insika/doctor.rb +716 -6
  69. data/lib/insika/dsl/runtime.rb +9 -3
  70. data/lib/insika/dsl/server_boot.rb +70 -1
  71. data/lib/insika/dsl.rb +83 -3
  72. data/lib/insika/edge_limiter.rb +28 -7
  73. data/lib/insika/env_schema.rb +9 -2
  74. data/lib/insika/errors.rb +11 -0
  75. data/lib/insika/evals/assertions.rb +3 -2
  76. data/lib/insika/evals/pairwise.rb +18 -6
  77. data/lib/insika/evidence.rb +183 -0
  78. data/lib/insika/executor.rb +687 -70
  79. data/lib/insika/followup_engine.rb +207 -0
  80. data/lib/insika/followup_policy.rb +221 -0
  81. data/lib/insika/followup_store.rb +306 -0
  82. data/lib/insika/funnel_declaration.rb +106 -0
  83. data/lib/insika/funnel_fold.rb +179 -0
  84. data/lib/insika/funnel_store.rb +163 -0
  85. data/lib/insika/golden_store.rb +1 -1
  86. data/lib/insika/grounding/matcher.rb +69 -0
  87. data/lib/insika/grounding.rb +44 -0
  88. data/lib/insika/harvest/conversion_gate.rb +159 -0
  89. data/lib/insika/harvest/criterion.rb +98 -0
  90. data/lib/insika/harvest/gate.rb +194 -0
  91. data/lib/insika/harvest/negative_list.rb +199 -0
  92. data/lib/insika/harvest.rb +241 -0
  93. data/lib/insika/harvest_engine.rb +193 -0
  94. data/lib/insika/harvest_store.rb +548 -0
  95. data/lib/insika/media.rb +298 -0
  96. data/lib/insika/memory_audit_store.rb +85 -0
  97. data/lib/insika/memory_store.rb +264 -23
  98. data/lib/insika/message_origin.rb +6 -1
  99. data/lib/insika/model_visible.rb +87 -0
  100. data/lib/insika/model_visible_trace_store.rb +66 -0
  101. data/lib/insika/onboarding.rb +6 -1
  102. data/lib/insika/outbox_store.rb +42 -4
  103. data/lib/insika/outcome_store.rb +147 -0
  104. data/lib/insika/packaging.rb +163 -0
  105. data/lib/insika/parity/criterion.rb +79 -0
  106. data/lib/insika/parity/verdict.rb +318 -0
  107. data/lib/insika/prefix_fingerprint.rb +58 -0
  108. data/lib/insika/profile_source.rb +22 -1
  109. data/lib/insika/proposal_store.rb +271 -0
  110. data/lib/insika/queue_policy.rb +4 -1
  111. data/lib/insika/refinement/proposer.rb +1 -1
  112. data/lib/insika/reliability.rb +32 -6
  113. data/lib/insika/retention.rb +281 -0
  114. data/lib/insika/routing.rb +101 -0
  115. data/lib/insika/safety/config.rb +44 -4
  116. data/lib/insika/safety/corpus.rb +255 -0
  117. data/lib/insika/safety/detectors.rb +32 -113
  118. data/lib/insika/safety/factory.rb +15 -2
  119. data/lib/insika/safety/grounding_enforcer.rb +59 -0
  120. data/lib/insika/safety/grounding_validator.rb +49 -0
  121. data/lib/insika/safety/input_guardrail.rb +1 -1
  122. data/lib/insika/safety/moderator.rb +1 -1
  123. data/lib/insika/safety/output_filter.rb +9 -5
  124. data/lib/insika/safety/output_validator.rb +11 -5
  125. data/lib/insika/schema_guard.rb +35 -0
  126. data/lib/insika/server/app.rb +144 -4
  127. data/lib/insika/server/rack_app.rb +16 -0
  128. data/lib/insika/server/responses.rb +40 -1
  129. data/lib/insika/session_actor.rb +8 -4
  130. data/lib/insika/session_store.rb +65 -2
  131. data/lib/insika/settings_store.rb +10 -0
  132. data/lib/insika/shadow_pair_store.rb +258 -0
  133. data/lib/insika/skill_catalog.rb +4 -0
  134. data/lib/insika/soak/envelope.rb +140 -0
  135. data/lib/insika/soak/report.rb +392 -0
  136. data/lib/insika/soak/runner.rb +554 -0
  137. data/lib/insika/store.rb +10 -1
  138. data/lib/insika/stores/memory.rb +6 -0
  139. data/lib/insika/stores/sqlite.rb +8 -0
  140. data/lib/insika/studio/app.rb +875 -14
  141. data/lib/insika/studio/assets/dist/application.css +1 -1
  142. data/lib/insika/studio/assets/dist/application.js +22 -22
  143. data/lib/insika/studio/assets/dist/favicon.svg +6 -0
  144. data/lib/insika/studio/forms.rb +262 -5
  145. data/lib/insika/studio/nav_icons.rb +6 -1
  146. data/lib/insika/studio/views/agent_detail.erb +627 -84
  147. data/lib/insika/studio/views/agents.erb +10 -6
  148. data/lib/insika/studio/views/approvals.erb +4 -1
  149. data/lib/insika/studio/views/chats.erb +4 -1
  150. data/lib/insika/studio/views/customer.erb +94 -0
  151. data/lib/insika/studio/views/customers.erb +32 -0
  152. data/lib/insika/studio/views/evals.erb +4 -1
  153. data/lib/insika/studio/views/facts.erb +133 -0
  154. data/lib/insika/studio/views/followups.erb +125 -0
  155. data/lib/insika/studio/views/funnel.erb +106 -0
  156. data/lib/insika/studio/views/harvest.erb +234 -0
  157. data/lib/insika/studio/views/home.erb +2 -1
  158. data/lib/insika/studio/views/layout.erb +1 -0
  159. data/lib/insika/studio/views/parity.erb +147 -0
  160. data/lib/insika/studio/views/playground.erb +7 -1
  161. data/lib/insika/studio/views/session.erb +56 -1
  162. data/lib/insika/studio/views/settings.erb +2 -0
  163. data/lib/insika/studio/views/skills.erb +0 -1
  164. data/lib/insika/studio/views/system_files.erb +1 -1
  165. data/lib/insika/studio/views/task.erb +13 -0
  166. data/lib/insika/studio/views/tasks.erb +4 -1
  167. data/lib/insika/studio/views/tools.erb +0 -1
  168. data/lib/insika/task_store.rb +21 -1
  169. data/lib/insika/testing/store_contract.rb +27 -6
  170. data/lib/insika/tick.rb +25 -1
  171. data/lib/insika/tool_definition.rb +17 -7
  172. data/lib/insika/tool_envelope.rb +69 -0
  173. data/lib/insika/tool_manifest.rb +5 -1
  174. data/lib/insika/tools/data_defined_tool.rb +10 -0
  175. data/lib/insika/tools/generate_image.rb +44 -0
  176. data/lib/insika/tools/schedule_followup.rb +164 -0
  177. data/lib/insika/tools/tts.rb +47 -0
  178. data/lib/insika/tools/update_briefing.rb +126 -0
  179. data/lib/insika/turn_state.rb +38 -1
  180. data/lib/insika/turn_timing.rb +22 -2
  181. data/lib/insika/version.rb +1 -1
  182. data/lib/insika/vitals.rb +84 -0
  183. data/lib/insika/wiring/graph.rb +300 -13
  184. data/lib/insika.rb +92 -3
  185. metadata +77 -1
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Insika
6
+ # the trigger — finds due sessions and mines them. It
7
+ # spawns a worker fiber (supervisor child, `Tick#start` shape) whose loop
8
+ # claims a window and runs ONE due session's harvest ON the worker fiber —
9
+ # off the tick's critical path and off every customer turn's path (D2: the
10
+ # mining model call never blocks a customer turn).
11
+ #
12
+ # The scan uses the engine default `idle_hours` (24) as the LOWER BOUND; the
13
+ # per-agent value is re-checked inside RunHarvest. The loop-stop (the
14
+ # H-harvest kill) is the profile's `enabled: false` — the engine only
15
+ # respects data (D4).
16
+ class HarvestEngine
17
+ SCOPE = "harvest_engine"
18
+ KEY = "claim"
19
+ DEFAULT_WINDOW = 300 # seconds — the O(n) scan never rides the 60 s loop
20
+ DEFAULT_IDLE_HOURS = 24
21
+ DEFAULT_MIN_MESSAGES = 3
22
+
23
+ def initialize(store:, harvest_store:, session_store:, runner:,
24
+ profiles: nil, logger: nil, window: DEFAULT_WINDOW,
25
+ idle_hours: DEFAULT_IDLE_HOURS, sleeper: nil)
26
+ @store = store
27
+ @harvest_store = harvest_store
28
+ @session_store = session_store
29
+ @runner = runner
30
+ @profiles = profiles # profile source; nil/empty = nothing mines (parity)
31
+ @logger = logger
32
+ @window = window.to_i
33
+ @idle_hours = idle_hours.to_i
34
+ @sleeper = sleeper || method(:default_sleep)
35
+ end
36
+
37
+ # The deployment root swaps in its own runner (the bus instance carrying
38
+ # the negative list + the settings-backed miner factory); the base graph's
39
+ # inert one keeps the loop honest until then.
40
+ attr_accessor :runner
41
+
42
+ # The supervisor child (tick.rb:69-83's shape). Returns false when
43
+ # idle_hours <= 0 (the engine default OFF switch — parity) OR when no
44
+ # profile declares an enabled harvest (a scan with nothing to mine would
45
+ # re-read every session record every window forever).
46
+ def start(parent:)
47
+ return false unless harvestable?
48
+
49
+ @task = parent.async do |t|
50
+ t.annotate("insika-harvest")
51
+ loop do
52
+ @sleeper.call(@window)
53
+ run_once
54
+ rescue StandardError => e
55
+ log(:warn, "harvest pass failed: #{e.class}: #{e.message}")
56
+ end
57
+ end
58
+ true
59
+ end
60
+
61
+ # One pass. -> { claimed: false } | { claimed: true, mined: N,
62
+ # skipped: N, errors: N }
63
+ def run_once
64
+ return { claimed: false } unless harvestable?
65
+ return { claimed: false } unless claim_window
66
+
67
+ session = due_sessions.first # one model call per pass; the window paces
68
+ return { claimed: true, mined: 0, skipped: 0, errors: 0 } if session.nil?
69
+
70
+ begin
71
+ outcome = @runner.call(Insika::Command.build(:run_harvest,
72
+ { agent: session[:agent],
73
+ session_ids: [session[:id]] }))
74
+ if outcome[:mined]
75
+ { claimed: true, mined: 1, skipped: 0, errors: 0 }
76
+ else
77
+ { claimed: true, mined: 0, skipped: 1, errors: 0 }
78
+ end
79
+ rescue StandardError
80
+ # a broken session must not hold the pass; the marker discipline
81
+ # keeps it re-runnable.
82
+ { claimed: true, mined: 0, skipped: 0, errors: 1 }
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ # The engine is inert unless the switch is on (idle_hours > 0) AND at
89
+ # least one profile declares an enabled harvest (the loop-stop: the
90
+ # H-harvest kill is `enabled: false`). The grounding matcher is a
91
+ # per-session pre-filter (D3), never an engine-wide switch.
92
+ def harvestable?
93
+ return false if @idle_hours <= 0
94
+ return false if @profiles.nil?
95
+
96
+ profiles_list.any? do |p|
97
+ config = p.respond_to?(:harvest) ? p.harvest : nil
98
+ config && Coercion.truthy?(config["enabled"])
99
+ end
100
+ end
101
+
102
+ def matcher_configured?(profile)
103
+ grounding = Grounding.parse(profile.grounding)
104
+ grounding && grounding.matcher.sku?
105
+ rescue StandardError
106
+ false
107
+ end
108
+
109
+ def profiles_list
110
+ return @profiles.values if @profiles.respond_to?(:values)
111
+
112
+ @profiles.respond_to?(:all) ? @profiles.all : []
113
+ end
114
+
115
+ # Sessions with `vars["agent"]`, idle past the PROFILE's own bounds, with
116
+ # enough messages, not yet mined, whose agent declares harvest.enabled + a
117
+ # matcher. Oldest first. One session per pass is enough (each is a model
118
+ # call).
119
+ #
120
+ # The scan reads the per-agent idle_hours/min_messages (the engine
121
+ # defaults only when the pack does not declare them): a session the pack's
122
+ # bounds reject is NOT elected, so it can never sit at the head of the
123
+ # queue — RunHarvest would skip it, write no marker, and it would be
124
+ # "the oldest" forever, starving every later session (the review fix).
125
+ def due_sessions
126
+ due = []
127
+ @session_store.each_id do |id|
128
+ session = @session_store.find(id)
129
+ next unless session
130
+ next if @harvest_store.mined?(id)
131
+ next if Coercion.blank?(session.vars["agent"])
132
+
133
+ profile = @profiles[session.vars["agent"].to_s]
134
+ next unless profile
135
+ next unless harvest_declared?(profile)
136
+
137
+ config = profile.respond_to?(:harvest) ? profile.harvest : nil
138
+ idle_hours = config&.key?("idle_hours") ? config["idle_hours"].to_i : @idle_hours
139
+ min_messages = config&.key?("min_messages") ? config["min_messages"].to_i : DEFAULT_MIN_MESSAGES
140
+ next unless aged?(session.updated_at, Time.now.utc - idle_hours * 3600)
141
+ next if session.messages.size < min_messages
142
+
143
+ due << { id: id, agent: session.vars["agent"].to_s, updated_at: session.updated_at }
144
+ end
145
+ due.sort_by { |s| s[:updated_at].to_s }
146
+ end
147
+
148
+ def harvest_declared?(profile)
149
+ config = profile.respond_to?(:harvest) ? profile.harvest : nil
150
+ return false unless config && Coercion.truthy?(config["enabled"])
151
+
152
+ matcher_configured?(profile)
153
+ end
154
+
155
+ def aged?(updated_at, cutoff)
156
+ return false if Coercion.blank?(updated_at)
157
+
158
+ Time.iso8601(updated_at.to_s) <= cutoff
159
+ rescue ArgumentError
160
+ false
161
+ end
162
+
163
+ # One worker per window across N workers — the tick's claim idiom.
164
+ def claim_window
165
+ now = Time.now.utc
166
+ @store.transaction do
167
+ current = @store.get(SCOPE, KEY)
168
+ last = current && begin
169
+ Time.iso8601(current["claimed_at"].to_s)
170
+ rescue ArgumentError
171
+ nil # a corrupted claim is not a claim — take the window
172
+ end
173
+ if last.nil? || (now - last) >= @window
174
+ @store.set(SCOPE, KEY, { "claimed_at" => now.iso8601 })
175
+ true
176
+ else
177
+ false
178
+ end
179
+ end
180
+ end
181
+
182
+ def default_sleep(seconds)
183
+ task = defined?(Async::Task) ? Async::Task.current? : nil
184
+ task ? task.sleep(seconds) : sleep(seconds)
185
+ end
186
+
187
+ def log(level, message)
188
+ @logger&.public_send(level, "[harvest] #{message}")
189
+ rescue StandardError
190
+ nil
191
+ end
192
+ end
193
+ end