Module: Xeno::Inputs
- Defined in:
- lib/xeno/inputs.rb
Overview
Resolves parked work: approvals, denials, and question answers. Each resolution updates its action; when nothing on the turn still awaits input, the turn flips back to pending and the resume job enqueues. A parked session holds no process — this is the only way back in.
Class Method Summary collapse
- .answer!(action, answer, principal: nil) ⇒ Object
- .approve!(action, principal: nil) ⇒ Object
- .deny!(action, reason: nil, principal: nil) ⇒ Object
-
.mirror_decision(action, decision) ⇒ Object
The action row stays authoritative (kind, payload, who, when); the decision is mirrored onto the standard tool-call column so any RubyLLM-aware tooling reads it natively.
-
.resolve(action) ⇒ Object
Guards the state, applies the resolution, and resumes when the turn has nothing left to wait for.
-
.resume_turn(turn) ⇒ Object
Resuming with unanswered inputs would generate against partial tool results — an invalid provider state the runner would re-park on.
Class Method Details
.answer!(action, answer, principal: nil) ⇒ Object
30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'lib/xeno/inputs.rb', line 30 def answer!(action, answer, principal: nil) raise ArgumentError, "not a question: #{action.tool_name}" unless action.kind == "question" resolve(action) do action.update!( status: "completed", output: { "content" => JSON.generate({ answer: answer }) }, resolved_at: Time.current, resolved_by: principal ) end end |
.approve!(action, principal: nil) ⇒ Object
10 11 12 13 14 15 |
# File 'lib/xeno/inputs.rb', line 10 def approve!(action, principal: nil) resolve(action) do action.update!(status: "approved", resolved_at: Time.current, resolved_by: principal) mirror_decision(action, "approved") end end |
.deny!(action, reason: nil, principal: nil) ⇒ Object
17 18 19 20 21 22 23 24 25 26 27 28 |
# File 'lib/xeno/inputs.rb', line 17 def deny!(action, reason: nil, principal: nil) resolve(action) do content = JSON.generate({ denied: true, reason: reason || "denied by user" }) action.update!( status: "denied", output: { "content" => content }, resolved_at: Time.current, resolved_by: principal ) mirror_decision(action, "denied") end end |
.mirror_decision(action, decision) ⇒ Object
The action row stays authoritative (kind, payload, who, when); the decision is mirrored onto the standard tool-call column so any RubyLLM-aware tooling reads it natively. Questions have no place there and are not mirrored.
46 47 48 49 50 51 |
# File 'lib/xeno/inputs.rb', line 46 def mirror_decision(action, decision) return unless action.kind == "tool" record = RubyLLM::ActiveRecord::ToolCall.find_by(tool_call_id: action.tool_call_id) record&.update!(approval: decision) end |
.resolve(action) ⇒ Object
Guards the state, applies the resolution, and resumes when the turn has nothing left to wait for. Resolution, status flip, and resume enqueue commit in one transaction, so on a DB-backed queue a crash cannot separate "approved" from "job exists". Backends that defer or lose the enqueue fall back to the reaper.
57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/xeno/inputs.rb', line 57 def resolve(action) unless action.status == "pending_approval" raise Xeno::Error, "action #{action.id} is not awaiting input (status: #{action.status})" end ActiveRecord::Base.transaction do yield resume_turn(action.turn) end action end |
.resume_turn(turn) ⇒ Object
Resuming with unanswered inputs would generate against partial tool results — an invalid provider state the runner would re-park on. Wake it only when everything is resolved.
71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/xeno/inputs.rb', line 71 def resume_turn(turn) return if turn.actions.where(status: "pending_approval").exists? # Resumes are human-driven and unbounded — counted apart from the failure `attempts` so # approvals can never poison the turn. resumed = Turn.where(id: turn.id, status: "waiting") .update_all("status = 'pending', resumes = resumes + 1") return unless resumed == 1 turn.session.update!(status: "running") if turn.session.status == "waiting" turn.enqueue! turn end |