Class: Protege::Persona
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Protege::Persona
- Includes:
- BroadcastablePersona, ToolScoped
- Defined in:
- app/models/protege/persona.rb
Overview
Active Record base class for every Protege persona — the agent identity behind an email address.
Personas are single-table-inheritance (STI) records: each concrete agent is a subclass defined
in the host application and persisted in protege_personas, discriminated by the type column.
A persona owns the conversations addressed to it (+EmailThread+, Message) and declares the
resolver chain that the Orchestrator runs to build the inference context for each turn.
The email_address is operator-chosen and must line up with the host's MX records. Inbound
routing supports RFC 5233 subaddressing: +alice+thread123@example.com+ routes to the persona
whose canonical address is alice@example.com (see Persona.lookup).
Constant Summary
Constants included from BroadcastablePersona
Class Method Summary collapse
-
.display_name ⇒ String?
Return the human-readable label for this persona subclass.
-
.display_name=(val) ⇒ String
Set the human-readable label for this persona subclass.
-
.lookup(address) ⇒ Protege::Persona?
Find the active persona that owns the given address, honouring subaddressing.
-
.message_resolver(klass, *args, **kwargs) ⇒ Class
Append a single resolver to the reply chain.
-
.message_resolvers {|chain| ... } ⇒ Orchestrator::ResolverChain
Return the resolver chain that assembles context for threaded email replies — the reactive path — optionally mutating it in a block.
-
.responsibility_resolver(klass, *args, **kwargs) ⇒ Class
Append a single resolver to the responsibility chain.
-
.responsibility_resolvers {|chain| ... } ⇒ Orchestrator::ResolverChain
Return the resolver chain that assembles context for scheduled responsibility runs — the proactive path (the Loop layer) — optionally mutating it in a block.
Instance Method Summary collapse
-
#active? ⇒ Boolean
Whether the persona is active (not archived).
-
#archive! ⇒ void
Archive the persona — stops inbound routing and scheduled responsibility runs; data is preserved.
-
#archived? ⇒ Boolean
Whether the persona has been archived (soft-removed): out of routing + scheduled runs, data kept.
-
#find_attachment(id:) ⇒ ActiveStorage::Attachment?
Find one of this persona's own message attachments by its Active Storage attachment id.
-
#message_resolvers ⇒ Orchestrator::ResolverChain
Return this persona's resolver chains by delegating to the class-level chains — the reply chain (+message_resolvers+) and the scheduled-run chain (+responsibility_resolvers+).
-
#unarchive! ⇒ void
Return an archived persona to active service.
Methods included from ToolScoped
#available_tool_ids, #available_tools
Class Method Details
.display_name ⇒ String?
Return the human-readable label for this persona subclass.
Falls back to the demodulized class name when no explicit label has been set, so an
unconfigured Foo::Agent subclass reads as "Agent".
93 94 95 |
# File 'app/models/protege/persona.rb', line 93 def display_name _display_name || name&.demodulize end |
.display_name=(val) ⇒ String
Set the human-readable label for this persona subclass.
101 102 103 |
# File 'app/models/protege/persona.rb', line 101 def display_name=(val) self._display_name = val end |
.lookup(address) ⇒ Protege::Persona?
Find the active persona that owns the given address, honouring subaddressing.
Parses the address through Gateway::Mail::Address and matches its routing_key (the
tag-stripped, lowercased local@domain) against the canonical email_address. So
+Alice+thread9@Example.com+ resolves to the persona at alice@example.com. Returns nil for
blank or malformed input.
165 166 167 168 169 |
# File 'app/models/protege/persona.rb', line 165 def lookup(address) active.find_by(email_address: Gateway.routing_key(address)) rescue ArgumentError nil end |
.message_resolver(klass, *args, **kwargs) ⇒ Class
Append a single resolver to the reply chain.
125 126 127 128 |
# File 'app/models/protege/persona.rb', line 125 def (klass, *args, **kwargs) { |chain| chain.use(klass, *args, **kwargs) } self end |
.message_resolvers {|chain| ... } ⇒ Orchestrator::ResolverChain
Return the resolver chain that assembles context for threaded email replies — the reactive path — optionally mutating it in a block.
Each subclass lazily builds and owns its own Orchestrator::ResolverChain; chains are not
shared with the parent class. Passing a block yields the chain so resolvers can be appended.
113 114 115 116 117 |
# File 'app/models/protege/persona.rb', line 113 def (&block) @message_resolvers ||= Orchestrator::ResolverChain.new block&.call(@message_resolvers) @message_resolvers end |
.responsibility_resolver(klass, *args, **kwargs) ⇒ Class
Append a single resolver to the responsibility chain.
151 152 153 154 |
# File 'app/models/protege/persona.rb', line 151 def responsibility_resolver(klass, *args, **kwargs) responsibility_resolvers { |chain| chain.use(klass, *args, **kwargs) } self end |
.responsibility_resolvers {|chain| ... } ⇒ Orchestrator::ResolverChain
Return the resolver chain that assembles context for scheduled responsibility runs — the proactive path (the Loop layer) — optionally mutating it in a block.
Held separately from the reply chain so a persona can present different context when acting on its own initiative (e.g. a task prompt, no inbound thread) than when answering mail. Like the reply chain it is lazily built and owned per subclass, never shared with the parent.
139 140 141 142 143 |
# File 'app/models/protege/persona.rb', line 139 def responsibility_resolvers(&block) @responsibility_resolvers ||= Orchestrator::ResolverChain.new block&.call(@responsibility_resolvers) @responsibility_resolvers end |
Instance Method Details
#active? ⇒ Boolean
Whether the persona is active (not archived).
188 189 190 |
# File 'app/models/protege/persona.rb', line 188 def active? archived_at.nil? end |
#archive! ⇒ void
This method returns an undefined value.
Archive the persona — stops inbound routing and scheduled responsibility runs; data is preserved.
202 203 204 |
# File 'app/models/protege/persona.rb', line 202 def archive! update!(archived_at: Time.current) end |
#archived? ⇒ Boolean
Whether the persona has been archived (soft-removed): out of routing + scheduled runs, data kept.
195 196 197 |
# File 'app/models/protege/persona.rb', line 195 def archived? archived_at.present? end |
#find_attachment(id:) ⇒ ActiveStorage::Attachment?
Find one of this persona's own message attachments by its Active Storage attachment id.
Scopes the lookup to attachments whose record is one of this persona's messages. The file tools
now resolve by blob id (see Protege::StoredFile.find), so this persona-scoped lookup is
currently unused by them — it is retained to become the scoping filter for the deferred
authorization pass (see the blob-currency spec §9). Returns nil when the id is unknown or
belongs to a different persona.
223 224 225 226 227 |
# File 'app/models/protege/persona.rb', line 223 def (id:) ActiveStorage::Attachment .where(name: 'attachments', record_type: 'Protege::Message', record_id: .select(:id)) .find_by(id:) end |
#message_resolvers ⇒ Orchestrator::ResolverChain
Return this persona's resolver chains by delegating to the class-level chains — the reply chain (+message_resolvers+) and the scheduled-run chain (+responsibility_resolvers+).
178 |
# File 'app/models/protege/persona.rb', line 178 delegate :message_resolvers, :responsibility_resolvers, to: :class |
#unarchive! ⇒ void
This method returns an undefined value.
Return an archived persona to active service.
209 210 211 |
# File 'app/models/protege/persona.rb', line 209 def unarchive! update!(archived_at: nil) end |