Class: Insika::Commands::RunHarvest
- Inherits:
-
Object
- Object
- Insika::Commands::RunHarvest
- Defined in:
- lib/insika/commands/run_harvest.rb
Overview
the ONLY path that writes candidates. Mines ONE window end to end: resolve the sessions, read the transcripts + evidence, ask the miner, schema-drop, apply the negative list and the grounding filter, dedup against the ledger, write the run + candidates, stamp the markers.
Synchronous (it runs on the engine's worker fiber, C12, or the CLI, C15) — it creates no task and no turn. It writes NOTHING to sessions or skills: D2's fork discipline is this command's contract — a customer turn's prefix is untouched by construction, and the only harvest-side spend is the run's cost (E1).
Payload: { agent:, last_sessions?, since?, full?, session_ids?, max_proposals?, exclude_sessions? }
Constant Summary collapse
- DEFAULT_LAST_SESSIONS =
the EvidenceCollector's window default
200- DEFAULT_MIN_MESSAGES =
3- DEFAULT_MAX_PROPOSALS =
10- DEFAULT_IDLE_HOURS =
24
Instance Method Summary collapse
-
#call(command) ⇒ Object
-> { mined: true, run_id:, candidates: N, rejected: { "
" => N, "ungrounded" => N, "dedup" => N, "schema" => N, ... }, cost: ... | nil } | { mined: false, skipped: "disabled|no_model|no_grounding_matcher" }. -
#initialize(profiles:, harvest_store:, session_store:, task_store:, skill_store: nil, tool_trace_store: nil, settings_store: nil, negative_list: nil, miner_factory: nil, event_stream:) ⇒ RunHarvest
constructor
A new instance of RunHarvest.
Constructor Details
#initialize(profiles:, harvest_store:, session_store:, task_store:, skill_store: nil, tool_trace_store: nil, settings_store: nil, negative_list: nil, miner_factory: nil, event_stream:) ⇒ RunHarvest
Returns a new instance of RunHarvest.
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/insika/commands/run_harvest.rb', line 26 def initialize(profiles:, harvest_store:, session_store:, task_store:, skill_store: nil, tool_trace_store: nil, settings_store: nil, negative_list: nil, miner_factory: nil, event_stream:) @profiles = ProfileSource.coerce(profiles) @harvest_store = harvest_store @session_store = session_store @task_store = task_store @skill_store = skill_store @tool_trace_store = tool_trace_store @settings_store = settings_store @negative_list = negative_list @miner_factory = miner_factory || ->(config) { Harvest::MinerFactory.build(config, utility_model: utility_model) } @event_stream = event_stream end |
Instance Method Details
#call(command) ⇒ Object
-> { mined: true, run_id:, candidates: N,
rejected: { "
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/insika/commands/run_harvest.rb', line 46 def call(command) p = AgentPayload.symbolize(command.payload) agent = AgentPayload.presence(p[:agent]) raise Insika::ValidationError, "agent is required" if agent.nil? profile = @profiles[agent] || (raise Insika::NotFoundError, "agent '#{agent}' not configured") config = Coercion.deep_stringify(profile.harvest) return skip("disabled") if config.nil? || !Coercion.truthy?(config["enabled"]) # D4: the OPERATIVE negative list lives on the profile (hot-editable, # seeded by `insika harvest:negative import`); the injected list is # the deployment's fallback. The engine applies data, never authors it. @list = negative_list_for(config) # D3: product claims cannot be verified without a matcher, so NOTHING # mines — refused, not warned (D12's "by refusal, not by prompt"). grounding = Grounding.parse(profile.grounding) return skip("no_grounding_matcher") if grounding.nil? || !grounding.matcher.sku? miner = @miner_factory.call(config) return skip("no_model") if miner.nil? run = @harvest_store.create_run(agent_id: agent, window: window_record(p), budget: budget_cap(config)) sessions = resolve_sessions(agent, p, config) begin # No eligible sessions: a run that says "we looked and it was clean" # without paying a bill; markers untouched (re-scan, D10). if sessions.empty? @harvest_store.complete_run(run.id, candidates: 0) return { mined: true, run_id: run.id, candidates: 0, rejected: empty_rejected, cost: nil } end prompt = build_prompt(config, sessions, agent) result = miner.mine(prompt: prompt, message_counts: sessions.map { |s| s[:messages].size }, max_proposals: max_proposals(p, config)) # The mining budget is a REAL cap (the review fix): a pass that # spent more than the pack declared is failed with the numbers and # proposes NOTHING — the docs' "the budget cap bounds it" is # enforced here, post-hoc for the one model call, pre-hoc for every # downstream write and gate. if budget_exceeded?(budget_cap(config), result[:cost]) @harvest_store.fail_run(run.id, error: "mining budget exceeded: " \ "spent #{result[:cost]['spent']} > " \ "#{budget_cap(config)['tokens']}") emit(:harvest_mined, agent: agent, run_id: run.id, candidates: 0, rejected: empty_rejected, cost: result[:cost]) return { mined: true, run_id: run.id, candidates: 0, rejected: empty_rejected, cost: result[:cost] } end survivors, rejected = filter_skills(result[:skills], sessions, agent, grounding.matcher) survivors.each do |skill| @harvest_store.create_candidate( run_id: run.id, agent: agent, name: skill["name"], description: skill["description"], body: skill_md(skill), triggers: skill["triggers"] || [], rationale: skill["rationale"].to_s, origin: sessions.map { |s| s[:id] }, evidence_turns: skill["evidence_turns"] || [], proposer: miner.model ) end final_rejected = merge_rejected(result[:dropped], rejected) @harvest_store.complete_run(run.id, candidates: survivors.size, cost: result[:cost], rejected: final_rejected) # Markers AFTER the pass completes (D10's crash-safe re-scan). sessions.each { |s| @harvest_store.mark_mined(s[:id], candidates: survivors.size) } emit(:harvest_mined, agent: agent, run_id: run.id, candidates: survivors.size, rejected: final_rejected, cost: result[:cost]) { mined: true, run_id: run.id, candidates: survivors.size, rejected: final_rejected, cost: result[:cost] } rescue StandardError => e # The run is failed; the markers are NOT written (re-scan, D10); # the exception propagates to the caller's fiber or the CLI. begin @harvest_store.fail_run(run.id, error: e.) rescue StandardError nil end raise end end |