Class: Silas::Channel
- Inherits:
-
Object
- Object
- Silas::Channel
- Defined in:
- lib/silas/channel.rb
Overview
A channel binds an external surface (email, Slack, HTTP) to the durable loop. Identity is the filename (app/agent/channels/slack.rb -> Agent::Channels::Slack).
Inbound is pure trigger reuse: dispatch maps an external thread to a Session via silas_sessions.channel + continuation_token, then calls the UNCHANGED public API (new thread -> Silas.agent.start; reply -> session.continue). Outbound (deliver the agent's answer / an approval request) is a subclass responsibility, invoked off the loop by ChannelDeliveryJob — so the loop's determinism and the ledger's exactly-once are never touched.
Direct Known Subclasses
Constant Summary collapse
- TOKEN_PURPOSE =
"silas/channel".freeze
Class Method Summary collapse
- .channel_name ⇒ Object
-
.dispatch(thread_key:, input:, metadata: {}) ⇒ Object
The single inbound entry point for every transport.
-
.for_session(session) ⇒ Object
Resolve the channel instance that owns a session (for outbound delivery).
-
.namespaced(thread_key) ⇒ Object
Stable external-thread key, namespaced by channel so two channels can't collide.
-
.token_for(invocation, action) ⇒ Object
Signed token for email approve/decline links (no session state leaks into the URL).
- .verifier ⇒ Object
- .verify_token(token) ⇒ Object
Instance Method Summary collapse
-
#deliver_answer(session:, text:) ⇒ Object
--- outbound interface (subclasses implement) ---.
- #deliver_approval(session:, invocation:) ⇒ Object
Class Method Details
.channel_name ⇒ Object
14 |
# File 'lib/silas/channel.rb', line 14 def self.channel_name = name.demodulize.underscore |
.dispatch(thread_key:, input:, metadata: {}) ⇒ Object
The single inbound entry point for every transport.
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
# File 'lib/silas/channel.rb', line 20 def self.dispatch(thread_key:, input:, metadata: {}) token = namespaced(thread_key) if (session = Silas::Session.find_by(continuation_token: token)) session.continue(input: input) session else Silas.agent.start(input: input, metadata: , channel: channel_name, continuation_token: token) end rescue ActiveRecord::RecordNotUnique # Concurrent first-inbound race: the other request created the session; # treat this message as a continue. session = Silas::Session.find_by!(continuation_token: namespaced(thread_key)) session.continue(input: input) session end |
.for_session(session) ⇒ Object
Resolve the channel instance that owns a session (for outbound delivery).
38 39 40 41 42 43 |
# File 'lib/silas/channel.rb', line 38 def self.for_session(session) return nil if session.channel.blank? klass = Silas.config.channel_resolver&.call(session.channel) klass&.new end |
.namespaced(thread_key) ⇒ Object
Stable external-thread key, namespaced by channel so two channels can't collide.
17 |
# File 'lib/silas/channel.rb', line 17 def self.namespaced(thread_key) = "#{channel_name}:#{thread_key}" |
.token_for(invocation, action) ⇒ Object
Signed token for email approve/decline links (no session state leaks into the URL).
46 47 48 |
# File 'lib/silas/channel.rb', line 46 def self.token_for(invocation, action) verifier.generate({ "id" => invocation.id, "action" => action.to_s }, expires_in: Silas.config.approval_ttl) end |
.verifier ⇒ Object
56 57 58 |
# File 'lib/silas/channel.rb', line 56 def self.verifier Rails.application.(TOKEN_PURPOSE) end |
.verify_token(token) ⇒ Object
50 51 52 53 54 |
# File 'lib/silas/channel.rb', line 50 def self.verify_token(token) verifier.verify(token) rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage nil end |
Instance Method Details
#deliver_answer(session:, text:) ⇒ Object
--- outbound interface (subclasses implement) ---
61 62 63 |
# File 'lib/silas/channel.rb', line 61 def deliver_answer(session:, text:) raise NotImplementedError, "#{self.class}#deliver_answer" end |
#deliver_approval(session:, invocation:) ⇒ Object
65 66 67 |
# File 'lib/silas/channel.rb', line 65 def deliver_approval(session:, invocation:) raise NotImplementedError, "#{self.class}#deliver_approval" end |