Class: Xeno::SessionsController
- Inherits:
-
ApiController
- Object
- ActionController::API
- ApiController
- Xeno::SessionsController
- Defined in:
- app/controllers/xeno/sessions_controller.rb
Overview
The HTTP channel: always mounted, fail-closed. The channel owns the continuation token (the resume handle): create returns one, follow-up messages must present it, terminal states release it, reset retires the session so the same token can start fresh.
Constant Summary
Constants inherited from ApiController
Instance Method Summary collapse
-
#cancel ⇒ Object
POST /v1/sessions/:id/cancel — stop the active turn, session intact.
-
#compact ⇒ Object
POST /v1/sessions/:id/compact — request a manual compaction.
-
#create ⇒ Object
POST /v1/sessions { message:, continuation_token: (optional) } Client-supplied tokens are restricted to the reserved
http:namespace — a craftedslack:<channel>:<ts>token would otherwise hijack which Slack thread the agent posts into. -
#input ⇒ Object
POST /v1/sessions/:id/inputs { action_id:, decision: approve|deny, reason: } or { action_id:, answer: }.
-
#message ⇒ Object
POST /v1/sessions/:id/messages { message:, continuation_token:, steer: } steer: true stops the active turn (settled safely) and makes this message the next turn instead of queueing behind the current work.
-
#reset ⇒ Object
POST /v1/sessions/:id/reset — retire the session (the /new command); releases the continuation token so the same handle can start fresh.
Instance Method Details
#cancel ⇒ Object
POST /v1/sessions/:id/cancel — stop the active turn, session intact. A parked/pending turn cancels immediately (no process holds it); a running turn is cancelled cooperatively via the persisted flag the runner's checker polls (takes effect within a chunk or step boundary).
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'app/controllers/xeno/sessions_controller.rb', line 80 def cancel session = find_owned_session! turn = session.turns.where(status: %w[pending running waiting]).order(:sequence).first return render json: { error: "no active turn" }, status: :conflict unless turn if turn.status == "running" session.chat.cancel! render json: { turn_id: turn.id, cancelling: true }, status: :accepted else # Settle BEFORE cancelling: a parked turn's assistant tool_calls are # still unanswered, and leaving them dangling bricks the session # (every later generate replays the malformed transcript). session.settle_unanswered_tool_calls!(turn, reason: "cancelled by user") turn.update!(status: "cancelled") session.update!(status: "running") if session.status == "waiting" session.emit("turn.cancelled", { turn_id: turn.id }) render json: { turn_id: turn.id, cancelled: true } end end |
#compact ⇒ Object
POST /v1/sessions/:id/compact — request a manual compaction. It runs as a claimed turn, so it queues behind any active or parked turn and executes the moment the session is free. Never appends a user message.
104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'app/controllers/xeno/sessions_controller.rb', line 104 def compact session = find_owned_session! return render json: { error: "session is finished" }, status: :gone unless session.active? if session.turns.where(kind: "compaction", status: %w[pending running]).exists? return render json: { error: "a compaction is already queued" }, status: :conflict end turn = session.stage_compaction_turn!(reason: "manual") turn.enqueue! render json: { session_id: session.id, turn_id: turn.id, status: "requested" }, status: :accepted end |
#create ⇒ Object
POST /v1/sessions { message:, continuation_token: (optional) }
Client-supplied tokens are restricted to the reserved http:
namespace — a crafted slack:<channel>:<ts> token would otherwise
hijack which Slack thread the agent posts into.
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# File 'app/controllers/xeno/sessions_controller.rb', line 11 def create = params.require(:message) token = params[:continuation_token].presence if token && !token.start_with?("http:") return render json: { error: "client-supplied continuation_token must use the http: namespace" }, status: :unprocessable_entity end token ||= "http:#{SecureRandom.base58(24)}" session = Session.start!( message: , channel: "http", principal: current_principal, continuation_token: token ) render json: { session_id: session.id, continuation_token: session.continuation_token }, status: :created rescue ActiveRecord::RecordNotUnique render json: { error: "continuation_token already in use by an active session" }, status: :conflict end |
#input ⇒ Object
POST /v1/sessions/:id/inputs { action_id:, decision: approve|deny, reason: } or { action_id:, answer: }
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'app/controllers/xeno/sessions_controller.rb', line 56 def input session = find_owned_session! action = Action.joins(:turn).where(xeno_turns: { session_id: session.id }) .find(params.require(:action_id)) if action.kind == "question" Inputs.answer!(action, params.require(:answer), principal: current_principal) else case params.require(:decision) when "approve" then Inputs.approve!(action, principal: current_principal) when "deny" then Inputs.deny!(action, reason: params[:reason], principal: current_principal) else return render json: { error: "decision must be approve or deny" }, status: :unprocessable_entity end end render json: { action_id: action.id, status: action.reload.status, turn_status: action.turn.reload.status } end |
#message ⇒ Object
POST /v1/sessions/:id/messages { message:, continuation_token:, steer: } steer: true stops the active turn (settled safely) and makes this message the next turn instead of queueing behind the current work.
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'app/controllers/xeno/sessions_controller.rb', line 37 def session = find_owned_session! return render json: { error: "session is finished — start a new one" }, status: :gone unless session.active? supplied = params[:continuation_token].to_s unless supplied.present? && session.continuation_token.present? && ActiveSupport::SecurityUtils.secure_compare(supplied, session.continuation_token) return render json: { error: "continuation_token missing or wrong" }, status: :forbidden end content = params.require(:message) steer = ActiveModel::Type::Boolean.new.cast(params[:steer]) turn = steer ? session.steer!(content) : session.(content) render json: { session_id: session.id, queued: turn.nil?, turn_id: turn&.id, steered: steer || nil }.compact, status: :accepted end |
#reset ⇒ Object
POST /v1/sessions/:id/reset — retire the session (the /new command); releases the continuation token so the same handle can start fresh.
119 120 121 122 123 124 125 126 127 128 129 |
# File 'app/controllers/xeno/sessions_controller.rb', line 119 def reset session = find_owned_session! return render json: { error: "session already finished" }, status: :gone unless session.active? session.chat.cancel! if session.turns.where(status: "running").exists? released = session.continuation_token session.finish!("completed") render json: { session_id: session.id, released_continuation_token: released } end |