Class: Insika::RefinementStore
- Inherits:
-
Object
- Object
- Insika::RefinementStore
- Includes:
- Coercion
- Defined in:
- lib/insika/refinement_store.rb
Overview
REFINEMENT DOMAIN store (RFC-0013, phase A). One record per refinement RUN:
the window that was read, the ranked findings the EvidenceCollector produced,
and the run's outcome. RUNTIME data (it is derived from sessions/tasks/traces),
so it takes the raw store: like SessionStore/TaskStore — not the ConfigStore.
The key embeds the agent and the start timestamp:
"run:<agent_id>:<started_at>:<id>"
so list(SCOPE, "run:<agent>:") comes back CHRONOLOGICAL for that agent (the
Store contract orders lexicographically) and latest_for is its last element.
An agent id containing ":" would break that split, so it is rejected on write.
Phase A wrote no edits anywhere — a Run was a REPORT and every non-collecting status was terminal. Phase C (RFC-0013 §3.2) adds the rest of the lifecycle on the SAME record, additively: a gated candidate and the operator's decision.
collecting ─▶ completed ─▶ gating ─▶ awaiting_approval ─▶ applied
╰─▶ no_findings ╰─▶ rejected (the gate failed it,
╰─▶ failed or the operator did)
The approval lives here, not in PendingActionStore — a deliberate deviation
from §3.6. That store is coupled to a suspended TURN: ApproveAction resolves
the record and then calls executor.approve(task_id) to wake a fiber. A
refinement proposal has no turn and no fiber, so reusing it would mean inventing
a task id, a fake tool name, and a wake-up that must never do anything — and
the operator's approvals inbox would fill with rows that are not tool calls. The
property §3.6 actually wanted is durability across a kill -9, and this store
has had it since phase A.
Defined Under Namespace
Classes: Run
Constant Summary collapse
- SCOPE =
"refinements"- KEY_PREFIX =
"run:"- STATUSES =
%i[collecting completed gating awaiting_approval applied rejected no_findings failed].freeze
- OPEN =
OPEN means "this run will change without anyone asking": work is in flight (
collecting,gating) or a human owes it an answer (awaiting_approval). Everything else is terminal — INCLUDINGcompleted, which phase A already treated that way and which stays true: a report is finished, and gating one is a new deliberate action, not a continuation. (Wideningterminal?here is what the Studio's "latest report" lookup reads, so getting it wrong hides the report.) %i[collecting gating awaiting_approval].freeze
Instance Method Summary collapse
-
#awaiting_approval(limit: 20) ⇒ Object
-> [Run] every run parked on a human, most recent first.
-
#complete(id, findings:, excluded: 0) ⇒ Object
Closes a run with its findings.
-
#create(agent_id:, window: {}, id: SecureRandom.uuid, at: nil) ⇒ Object
Opens a run (:collecting).
-
#fail(id, error:) ⇒ Object
Closes a run as :failed, recording the error.
-
#find(id) ⇒ Object
-> Run | nil.
-
#for_agent(agent_id, limit: nil) ⇒ Object
-> [Run] for one agent, MOST RECENT FIRST, capped by
limit. -
#gated(id, report:, panel: nil, cost: nil) ⇒ Object
Records the gate's verdict.
-
#gating(id, candidate: nil, candidates: nil) ⇒ Object
Attaches the candidate(s) under gate and moves the run to :gating.
-
#initialize(store:) ⇒ RefinementStore
constructor
A new instance of RefinementStore.
-
#latest_for(agent_id) ⇒ Object
-> Run | nil (the agent's most recent run, whatever its status).
-
#recent(limit: 20) ⇒ Object
-> [Run] across every agent, most recent first, capped.
-
#resolve(id, decision:, operator: nil, note: nil) ⇒ Object
The operator's answer to a gated proposal.
Methods included from Coercion
blank?, deep_stringify, presence, present?, utf8
Constructor Details
#initialize(store:) ⇒ RefinementStore
Returns a new instance of RefinementStore.
71 72 73 |
# File 'lib/insika/refinement_store.rb', line 71 def initialize(store:) @store = store end |
Instance Method Details
#awaiting_approval(limit: 20) ⇒ Object
-> [Run] every run parked on a human, most recent first. What the Studio badges.
201 202 203 |
# File 'lib/insika/refinement_store.rb', line 201 def awaiting_approval(limit: 20) recent(limit: 200).select(&:awaiting_approval?).first(limit) end |
#complete(id, findings:, excluded: 0) ⇒ Object
Closes a run with its findings. Empty findings -> :no_findings (a distinct
outcome from :completed — "we looked and it was clean" is a real answer, not a
failure). -> Run. ArgumentError if the run is already terminal.
excluded is how many turns the window dropped on purpose (synthetic
traffic) — recorded so a report never reads cleaner than the data was.
100 101 102 103 104 105 106 107 108 109 |
# File 'lib/insika/refinement_store.rb', line 100 def complete(id, findings:, excluded: 0) update(id) do |record| guard_open!(record) list = Array(findings).map { |f| deep_stringify(f.respond_to?(:to_h) ? f.to_h : f) } record["findings"] = list record["excluded"] = Integer(excluded) record["status"] = list.empty? ? "no_findings" : "completed" record["finished_at"] = end end |
#create(agent_id:, window: {}, id: SecureRandom.uuid, at: nil) ⇒ Object
Opens a run (:collecting). window is the collector's window as data
({ "last_sessions" => N } | { "since" => iso8601 }) — recorded so a report can
be read months later and still say what it looked at. -> Run.
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/insika/refinement_store.rb', line 78 def create(agent_id:, window: {}, id: SecureRandom.uuid, at: nil) agent = agent_id.to_s raise Insika::ValidationError, "agent_id is required" if agent.empty? raise Insika::ValidationError, "agent_id must not contain ':'" if agent.include?(":") started = at || record = { "id" => id.to_s, "agent_id" => agent, "status" => "collecting", "window" => deep_stringify(window || {}), "findings" => [], "excluded" => 0, "started_at" => started, "finished_at" => nil, "error" => nil, "candidate" => nil, "candidates" => [], "gate" => nil, "cost" => nil, "decision" => nil } @store.set(SCOPE, key_for(agent, started, id), record) to_run(record) end |
#fail(id, error:) ⇒ Object
Closes a run as :failed, recording the error. -> Run.
112 113 114 115 116 117 118 119 |
# File 'lib/insika/refinement_store.rb', line 112 def fail(id, error:) update(id) do |record| guard_open!(record) record["status"] = "failed" record["error"] = error.to_s record["finished_at"] = end end |
#find(id) ⇒ Object
-> Run | nil. O(n) scan over the scope (the key carries agent+timestamp, so there is no index by id): one node, local SQLite, runs are operator-paced.
207 208 209 210 |
# File 'lib/insika/refinement_store.rb', line 207 def find(id) key = key_for_id(id) key && to_run(@store.get(SCOPE, key)) end |
#for_agent(agent_id, limit: nil) ⇒ Object
-> [Run] for one agent, MOST RECENT FIRST, capped by limit.
213 214 215 216 217 |
# File 'lib/insika/refinement_store.rb', line 213 def for_agent(agent_id, limit: nil) keys = @store.list(SCOPE, "#{KEY_PREFIX}#{agent_id}:").reverse keys = keys.first(limit) if limit keys.filter_map { |k| to_run(@store.get(SCOPE, k)) } end |
#gated(id, report:, panel: nil, cost: nil) ⇒ Object
Records the gate's verdict. A PASS parks the run at :awaiting_approval — a human still has to say yes, which is the product and not a formality (D2). A FAIL is terminal as :rejected, with the gate report as the stated reason: the same finding must re-surface with new evidence before anything is proposed again, so there is no silent retry loop (§3.6).
report is the WINNER's (or, when nothing survived, the most informative
refusal). panel is every scored entry — the store attaches it as-is and picks
the winning candidate out of it by id. Which candidate WON is the caller's
ranking decision (§3.5); this store does not rank, it records. -> Run.
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/insika/refinement_store.rb', line 157 def gated(id, report:, panel: nil, cost: nil) update(id) do |record| unless record["status"] == "gating" raise ArgumentError, "run #{record['id']} is #{record['status']}, expected gating" end gate = deep_stringify(report.respond_to?(:to_h) ? report.to_h : report) record["candidates"] = panel.map { |e| entry_for(e) } if panel record["candidate"] = winning_candidate(record, gate) record["gate"] = gate record["cost"] = deep_stringify(cost.respond_to?(:to_h) ? cost.to_h : cost) if cost if gate["passed"] record["status"] = "awaiting_approval" else record["status"] = "rejected" record["decision"] = { "by" => "gate", "at" => , "note" => gate["reason"] } record["finished_at"] = end end end |
#gating(id, candidate: nil, candidates: nil) ⇒ Object
Attaches the candidate(s) under gate and moves the run to :gating. Only a
completed run can be gated: a report with no findings has nothing to propose
from, and a failed one never finished looking.
candidate: (one) and candidates: (a panel) are the same call — the panel is
recorded before the gate runs so the Studio can show WHAT is being scored while
it is being scored, which is minutes of real replay. No winner is claimed yet:
candidate stays nil until gated says which one it is. -> Run.
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/insika/refinement_store.rb', line 131 def gating(id, candidate: nil, candidates: nil) panel = Array(candidates || [candidate].compact) raise Insika::ValidationError, "a candidate is required to gate" if panel.empty? update(id) do |record| unless record["status"] == "completed" raise ArgumentError, "run #{record['id']} is #{record['status']}, expected completed" end record["candidates"] = panel.map { |c| entry_for(c) } record["candidate"] = nil record["gate"] = nil record["status"] = "gating" end end |
#latest_for(agent_id) ⇒ Object
-> Run | nil (the agent's most recent run, whatever its status).
220 |
# File 'lib/insika/refinement_store.rb', line 220 def latest_for(agent_id) = for_agent(agent_id, limit: 1).first |
#recent(limit: 20) ⇒ Object
-> [Run] across every agent, most recent first, capped.
223 224 225 226 227 |
# File 'lib/insika/refinement_store.rb', line 223 def recent(limit: 20) @store.list(SCOPE, KEY_PREFIX) .filter_map { |k| to_run(@store.get(SCOPE, k)) } .sort_by { |r| r.started_at.to_s }.reverse.first(limit) end |
#resolve(id, decision:, operator: nil, note: nil) ⇒ Object
The operator's answer to a gated proposal. applied is recorded only after the
writes land, so a crash between the two leaves the run awaiting approval and
the operator re-approves — replaying a write that is already versioned and
idempotent-ish beats recording a lie. -> Run.
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/insika/refinement_store.rb', line 182 def resolve(id, decision:, operator: nil, note: nil) target = decision.to_sym unless %i[applied rejected].include?(target) raise Insika::ValidationError, "invalid decision: #{decision} (applied|rejected)" end update(id) do |record| unless record["status"] == "awaiting_approval" raise ArgumentError, "run #{record['id']} is #{record['status']}, expected awaiting_approval" end record["status"] = target.to_s record["decision"] = { "by" => (Coercion.presence(operator) || "operator").to_s, "at" => , "note" => Coercion.presence(note) }.compact record["finished_at"] = end end |