protege 0.1.0.alpha.2 → 0.1.0.alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +21 -0
  3. data/Rakefile +1 -3
  4. data/app/assets/builds/protege.css +76 -83
  5. data/app/controllers/concerns/protege/agent_scoped.rb +22 -0
  6. data/app/controllers/concerns/protege/agent_toolkit_scoped.rb +22 -0
  7. data/app/controllers/protege/access_rules_controller.rb +13 -13
  8. data/app/controllers/protege/agent_toolkit_rules_controller.rb +62 -0
  9. data/app/controllers/protege/agent_toolkits_controller.rb +44 -0
  10. data/app/controllers/protege/agents/agent_toolkits_controller.rb +65 -0
  11. data/app/controllers/protege/agents_controller.rb +118 -0
  12. data/app/controllers/protege/archives_controller.rb +18 -18
  13. data/app/controllers/protege/home_controller.rb +1 -1
  14. data/app/controllers/protege/messages_controller.rb +1 -1
  15. data/app/controllers/protege/replies_controller.rb +3 -3
  16. data/app/controllers/protege/responsibilities_controller.rb +7 -7
  17. data/app/controllers/protege/threads_controller.rb +4 -4
  18. data/app/controllers/protege/toolkits_controller.rb +84 -0
  19. data/app/helpers/protege/agents_helper.rb +75 -0
  20. data/app/helpers/protege/application_helper.rb +3 -2
  21. data/app/helpers/protege/components/badges_helper.rb +11 -2
  22. data/app/helpers/protege/components/forms_helper.rb +47 -12
  23. data/app/helpers/protege/components/typography_helper.rb +1 -1
  24. data/app/helpers/protege/messages_helper.rb +2 -2
  25. data/app/helpers/protege/responsibilities_helper.rb +2 -2
  26. data/app/helpers/protege/threads_helper.rb +11 -11
  27. data/app/helpers/protege/toolkits_helper.rb +66 -0
  28. data/app/hooks/protege/event_logger_hook.rb +8 -8
  29. data/app/jobs/protege/console_inference_job.rb +8 -8
  30. data/app/jobs/protege/inference_job.rb +11 -11
  31. data/app/jobs/protege/responsibility_job.rb +10 -10
  32. data/app/mailboxes/protege/agent_mailbox.rb +54 -29
  33. data/app/mailers/protege/alert_mailer.rb +41 -0
  34. data/app/mailers/protege/application_mailer.rb +9 -8
  35. data/app/models/concerns/protege/broadcastable_agent.rb +50 -0
  36. data/app/models/concerns/protege/broadcastable_toolkit.rb +49 -0
  37. data/app/models/concerns/protege/toolable.rb +108 -0
  38. data/app/models/protege/access_rule.rb +12 -12
  39. data/app/models/protege/{persona.rb → agent.rb} +45 -45
  40. data/app/models/protege/agent_toolkit.rb +50 -0
  41. data/app/models/protege/agent_toolkit_rule.rb +25 -0
  42. data/app/models/protege/application_record.rb +1 -1
  43. data/app/models/protege/email_thread.rb +16 -13
  44. data/app/models/protege/message.rb +36 -20
  45. data/app/models/protege/responsibility.rb +12 -12
  46. data/app/models/protege/tool_use.rb +6 -6
  47. data/app/models/protege/toolkit.rb +115 -0
  48. data/app/models/protege/trace.rb +1 -1
  49. data/app/providers/protege/open_router_provider.rb +17 -0
  50. data/app/resolvers/README.md +4 -4
  51. data/app/resolvers/protege/load_file_resolver.rb +5 -5
  52. data/app/resolvers/protege/load_text_resolver.rb +3 -3
  53. data/app/resolvers/protege/thread_history_resolver.rb +1 -1
  54. data/app/services/protege/message_search.rb +2 -2
  55. data/app/services/protege/system_toolkits.rb +67 -0
  56. data/app/tools/protege/create_file_tool.rb +2 -0
  57. data/app/tools/protege/read_attachment_tool.rb +3 -1
  58. data/app/tools/protege/search_emails_tool.rb +15 -13
  59. data/app/tools/protege/send_email_tool.rb +60 -18
  60. data/app/tools/protege/web_fetch_tool.rb +2 -0
  61. data/app/tools/protege/web_search_tool.rb +3 -1
  62. data/app/views/layouts/mailer.text.erb +1 -0
  63. data/app/views/protege/access_rules/create.turbo_stream.slim +3 -3
  64. data/app/views/protege/agent_toolkit_rules/create.turbo_stream.slim +6 -0
  65. data/app/views/protege/agent_toolkits/_gate_rule.html.slim +7 -0
  66. data/app/views/protege/agent_toolkits/_gate_rule_form.html.slim +8 -0
  67. data/app/views/protege/agent_toolkits/_gate_rules.html.slim +19 -0
  68. data/app/views/protege/agent_toolkits/show.html.slim +17 -0
  69. data/app/views/protege/{personas → agents}/_access_rule.html.slim +1 -1
  70. data/app/views/protege/{personas → agents}/_access_rule_form.html.slim +1 -1
  71. data/app/views/protege/{personas → agents}/_access_rules.html.slim +5 -5
  72. data/app/views/protege/agents/_actions.html.slim +15 -0
  73. data/app/views/protege/agents/_agent_sidebar_item.html.slim +5 -0
  74. data/app/views/protege/agents/_agent_toolkit.html.slim +7 -0
  75. data/app/views/protege/agents/_agent_toolkit_form.html.slim +7 -0
  76. data/app/views/protege/agents/_form.html.slim +25 -0
  77. data/app/views/protege/agents/_sidebar.html.slim +11 -0
  78. data/app/views/protege/agents/_toolkits.html.slim +19 -0
  79. data/app/views/protege/agents/agent_toolkits/create.turbo_stream.slim +6 -0
  80. data/app/views/protege/agents/edit.html.slim +7 -0
  81. data/app/views/protege/agents/index.html.slim +6 -0
  82. data/app/views/protege/agents/new.html.slim +7 -0
  83. data/app/views/protege/agents/show.html.slim +29 -0
  84. data/app/views/protege/alert_mailer/inference_failed.text.erb +4 -0
  85. data/app/views/protege/email_domains/show.html.slim +1 -1
  86. data/app/views/protege/home/show.html.slim +13 -13
  87. data/app/views/protege/responsibilities/_form.html.slim +2 -2
  88. data/app/views/protege/responsibilities/_responsibility_sidebar_item.html.slim +1 -1
  89. data/app/views/protege/responsibilities/new.html.slim +1 -1
  90. data/app/views/protege/responsibilities/show.html.slim +2 -2
  91. data/app/views/protege/responsibility_runs/show.html.slim +1 -1
  92. data/app/views/protege/shared/_header.html.slim +2 -1
  93. data/app/views/protege/shared/_hotkeys.html.slim +15 -11
  94. data/app/views/protege/threads/new.html.slim +2 -2
  95. data/app/views/protege/toolkits/_form.html.slim +13 -0
  96. data/app/views/protege/toolkits/_sidebar.html.slim +11 -0
  97. data/app/views/protege/toolkits/_toolkit_sidebar_item.html.slim +5 -0
  98. data/app/views/protege/toolkits/edit.html.slim +7 -0
  99. data/app/views/protege/toolkits/index.html.slim +6 -0
  100. data/app/views/protege/toolkits/new.html.slim +7 -0
  101. data/app/views/protege/toolkits/show.html.slim +13 -0
  102. data/config/routes.rb +12 -1
  103. data/db/migrate/20260707120000_replace_persona_active_with_archived_at.rb +1 -1
  104. data/db/migrate/20260708120000_add_disabled_tool_ids_to_personas.rb +1 -1
  105. data/db/migrate/20260728140000_scope_message_and_thread_uniqueness_per_persona.rb +16 -0
  106. data/db/migrate/20260812000001_create_protege_toolkits.rb +37 -0
  107. data/db/migrate/20260815000001_remove_disabled_tool_ids_from_personas.rb +21 -0
  108. data/db/migrate/20260815000002_add_key_to_protege_toolkits.rb +22 -0
  109. data/db/migrate/20260815100000_rename_personas_to_agents.rb +24 -0
  110. data/lib/generators/protege/agent/agent_generator.rb +35 -0
  111. data/lib/generators/protege/{persona/templates/persona.rb.tt → agent/templates/agent.rb.tt} +9 -9
  112. data/lib/generators/protege/extension_naming.rb +1 -1
  113. data/lib/generators/protege/hook/templates/hook.rb.tt +2 -2
  114. data/lib/generators/protege/install/install_generator.rb +16 -11
  115. data/lib/generators/protege/install/templates/initializer.rb.tt +17 -6
  116. data/lib/generators/protege/postfix/templates/deploy/mail/MAIL.md +2 -2
  117. data/lib/generators/protege/resolver/resolver_generator.rb +2 -2
  118. data/lib/generators/protege/resolver/templates/resolver.rb.tt +4 -4
  119. data/lib/generators/protege/tool/templates/tool.rb.tt +7 -5
  120. data/lib/protege/configuration.rb +43 -8
  121. data/lib/protege/engine.rb +3 -1
  122. data/lib/protege/errors/tool_not_available_error.rb +7 -6
  123. data/lib/protege/events/event.rb +2 -2
  124. data/lib/protege/events/inference_chunk_event.rb +1 -1
  125. data/lib/protege/events/inference_completed_event.rb +1 -1
  126. data/lib/protege/events/inference_failed_event.rb +1 -1
  127. data/lib/protege/events/inference_generated_event.rb +1 -1
  128. data/lib/protege/events/inference_max_turns_reached_event.rb +1 -1
  129. data/lib/protege/events/inference_started_event.rb +1 -1
  130. data/lib/protege/events/loop_run_completed_event.rb +1 -1
  131. data/lib/protege/events/loop_run_enqueued_event.rb +1 -1
  132. data/lib/protege/events/loop_run_failed_event.rb +1 -1
  133. data/lib/protege/events/loop_run_started_event.rb +1 -1
  134. data/lib/protege/events/tool_call_completed_event.rb +1 -1
  135. data/lib/protege/events/tool_call_failed_event.rb +1 -1
  136. data/lib/protege/events/tool_call_started_event.rb +1 -1
  137. data/lib/protege/events/tool_calls_received_event.rb +1 -1
  138. data/lib/protege/extensions/hook.rb +1 -0
  139. data/lib/protege/extensions/hook_mixin.rb +1 -1
  140. data/lib/protege/extensions/manifest.rb +67 -0
  141. data/lib/protege/extensions/manifested.rb +81 -0
  142. data/lib/protege/extensions/provider.rb +1 -0
  143. data/lib/protege/extensions/provider_mixin.rb +9 -17
  144. data/lib/protege/extensions/resolver.rb +2 -1
  145. data/lib/protege/extensions/resolver_mixin.rb +2 -2
  146. data/lib/protege/extensions/tool.rb +1 -0
  147. data/lib/protege/extensions/tool_mixin.rb +16 -40
  148. data/lib/protege/gateway/access_control.rb +19 -19
  149. data/lib/protege/gateway/access_policy.rb +2 -2
  150. data/lib/protege/gateway/mail/outbound.rb +22 -5
  151. data/lib/protege/gateway.rb +48 -37
  152. data/lib/protege/loop/scheduler.rb +1 -1
  153. data/lib/protege/orchestrator/context.rb +8 -8
  154. data/lib/protege/orchestrator/harness.rb +26 -24
  155. data/lib/protege/orchestrator/reply_context.rb +3 -3
  156. data/lib/protege/orchestrator/reply_harness.rb +8 -8
  157. data/lib/protege/orchestrator/resolver_chain.rb +3 -3
  158. data/lib/protege/orchestrator/responsibility_context.rb +4 -4
  159. data/lib/protege/orchestrator/responsibility_harness.rb +8 -8
  160. data/lib/protege/orchestrator.rb +7 -7
  161. data/lib/protege/subscribers/alerter.rb +117 -0
  162. data/lib/protege/subscribers/tracing.rb +1 -1
  163. data/lib/protege/version.rb +1 -1
  164. data/lib/protege.rb +2 -2
  165. data/lib/tasks/protege_tasks.rake +12 -0
  166. metadata +66 -23
  167. data/app/controllers/concerns/protege/persona_scoped.rb +0 -22
  168. data/app/controllers/protege/personas_controller.rb +0 -118
  169. data/app/helpers/protege/personas_helper.rb +0 -90
  170. data/app/models/concerns/protege/broadcastable_persona.rb +0 -50
  171. data/app/models/concerns/protege/tool_scoped.rb +0 -88
  172. data/app/views/protege/personas/_actions.html.slim +0 -15
  173. data/app/views/protege/personas/_form.html.slim +0 -29
  174. data/app/views/protege/personas/_persona_sidebar_item.html.slim +0 -5
  175. data/app/views/protege/personas/_sidebar.html.slim +0 -11
  176. data/app/views/protege/personas/edit.html.slim +0 -7
  177. data/app/views/protege/personas/index.html.slim +0 -6
  178. data/app/views/protege/personas/new.html.slim +0 -7
  179. data/app/views/protege/personas/show.html.slim +0 -37
  180. data/lib/generators/protege/persona/persona_generator.rb +0 -35
@@ -18,12 +18,12 @@ module Protege
18
18
  #
19
19
  # +Protege::Current.correlation_id+ must be set before calling so that emitted events carry the id
20
20
  # automatically. Collaborates with +Inference+ (request building and generation), +Tool+ (dispatch
21
- # and registry), the persona's +ResolverChain+ (context assembly), and the +Protege::*Event+ classes
21
+ # and registry), the agent's +ResolverChain+ (context assembly), and the +Protege::*Event+ classes
22
22
  # (observability).
23
23
  #
24
24
  # @example
25
- # ReplyHarness.run(persona:, message:)
26
- # ResponsibilityHarness.run(persona:, responsibility:)
25
+ # ReplyHarness.run(agent:, message:)
26
+ # ResponsibilityHarness.run(agent:, responsibility:)
27
27
  #
28
28
  # == Lifecycle
29
29
  #
@@ -45,10 +45,10 @@ module Protege
45
45
  end
46
46
  end
47
47
 
48
- # @param persona [Protege::Persona] the persona handling the run
48
+ # @param agent [Protege::Agent] the agent handling the run
49
49
  # @return [void]
50
- def initialize(persona:)
51
- @persona = persona
50
+ def initialize(agent:)
51
+ @agent = agent
52
52
  end
53
53
 
54
54
  # Entry point. Emit lifecycle events around the tool loop.
@@ -61,22 +61,22 @@ module Protege
61
61
  # @return [Object] the final +Provider::Response+
62
62
  # @raise [StandardError] re-raised after +inference_failed+ is emitted
63
63
  def run
64
- InferenceStartedEvent.emit(persona:, message:)
64
+ InferenceStartedEvent.emit(agent:, message:)
65
65
 
66
66
  result = run_tool_loop
67
67
 
68
- InferenceCompletedEvent.emit(persona:, message:, result:)
68
+ InferenceCompletedEvent.emit(agent:, message:, result:)
69
69
 
70
70
  result
71
71
  rescue StandardError => e
72
- InferenceFailedEvent.emit(persona:, message:, error: e)
72
+ InferenceFailedEvent.emit(agent:, message:, error: e)
73
73
  raise
74
74
  end
75
75
 
76
76
  private
77
77
 
78
- # @return [Protege::Persona] the persona handling the run
79
- attr_reader :persona
78
+ # @return [Protege::Agent] the agent handling the run
79
+ attr_reader :agent
80
80
 
81
81
  # ── Kind-specific hooks (subclasses override) ─────────────────────────────
82
82
 
@@ -134,11 +134,11 @@ module Protege
134
134
  return final if final.tool_calls.blank?
135
135
 
136
136
  if turn >= (max_loop_iterations - 1)
137
- InferenceMaxTurnsReachedEvent.emit(persona:, turn:, tool_calls: final.tool_calls)
137
+ InferenceMaxTurnsReachedEvent.emit(agent:, turn:, tool_calls: final.tool_calls)
138
138
  break
139
139
  end
140
140
 
141
- ToolCallsReceivedEvent.emit(persona:, tool_calls: final.tool_calls)
141
+ ToolCallsReceivedEvent.emit(agent:, tool_calls: final.tool_calls)
142
142
 
143
143
  tool_results = final.tool_calls.map { invoke_tool(_1) }
144
144
  record_tool_round(turn:, response: final, tool_results:)
@@ -175,7 +175,7 @@ module Protege
175
175
  # Emit the per-turn trace event: the wire request (+Request#to_wire+ — the very serialization the
176
176
  # API call uses, so the snapshot is exact), the response, and the inference settings that shaped it
177
177
  # (+model+ + +settings+, pulled from the provider the harness resolves — not global config — so a
178
- # future persona-level override is captured). Best-effort: any failure is logged and swallowed, so
178
+ # future agent-level override is captured). Best-effort: any failure is logged and swallowed, so
179
179
  # building or publishing the event can never abort a run whose side effects may already have run.
180
180
  #
181
181
  # @param turn [Integer] this generation's turn index
@@ -205,7 +205,7 @@ module Protege
205
205
  def run_generation(request)
206
206
  if streaming?
207
207
  Inference.generate_stream(request) do |chunk|
208
- InferenceChunkEvent.emit(persona:, thread: message.email_thread, chunk:)
208
+ InferenceChunkEvent.emit(agent:, thread: message.email_thread, chunk:)
209
209
  end
210
210
  else
211
211
  Inference.generate(request)
@@ -254,14 +254,14 @@ module Protege
254
254
  # @param tool_call [Object] the tool call to dispatch
255
255
  # @return [Protege::ToolResult] the call paired with its outcome
256
256
  def invoke_tool(tool_call)
257
- ToolCallStartedEvent.emit(persona:, tool_call:)
257
+ ToolCallStartedEvent.emit(agent:, tool_call:)
258
258
 
259
259
  tool_result = ToolMixin.invoke(tool_call, context:)
260
260
 
261
261
  if tool_result.result.success?
262
- ToolCallCompletedEvent.emit(persona:, tool_call:, result: tool_result.result)
262
+ ToolCallCompletedEvent.emit(agent:, tool_call:, result: tool_result.result)
263
263
  else
264
- ToolCallFailedEvent.emit(persona:, tool_call:, error: tool_result.result.error)
264
+ ToolCallFailedEvent.emit(agent:, tool_call:, error: tool_result.result.error)
265
265
  end
266
266
 
267
267
  tool_result
@@ -284,19 +284,21 @@ module Protege
284
284
  end
285
285
  end
286
286
 
287
- # Assemble the first +Provider::Request+ from the opening user turn, the persona's effective
287
+ # Assemble the first +Provider::Request+ from the opening user turn, the agent's effective
288
288
  # (scoped) tool set, inference config, and any context messages contributed by resolvers.
289
289
  #
290
- # The catalogue is +persona.available_tools+ rather than the whole registry, so a persona's
291
- # code-declared grant and the operator's +disabled_tool_ids+ override bound what the model is
292
- # offered — on both the reply and the scheduled path, since both inherit this base method. Later
293
- # turns re-use +previous.tools+ (see +Inference.extend_request+), so the scope holds for the run.
290
+ # The catalogue is +agent.available_tools_for(context:)+ rather than the whole registry, so the
291
+ # agent's attached toolkits — and each attachment's sender gate — bound what the model is offered,
292
+ # on both the reply and the scheduled path, since both inherit this base method (the context's
293
+ # sender, or its absence on a scheduled run, drives toolkit gating). Later turns re-use
294
+ # +previous.tools+ (see +Inference.extend_request+), and a reply run has one fixed sender, so the
295
+ # scope holds for the run.
294
296
  #
295
297
  # @return [Object] the initial provider request
296
298
  def build_initial_request
297
299
  Inference.build_request(
298
300
  user_content:,
299
- tools: persona.available_tools,
301
+ tools: agent.available_tools_for(context:),
300
302
  context_messages: resolvers.run(context)
301
303
  )
302
304
  end
@@ -9,12 +9,12 @@ module Protege
9
9
  # @return [Object] the inbound message being processed
10
10
  attr_reader :message
11
11
 
12
- # @param persona [Protege::Persona] the persona handling the run
12
+ # @param agent [Protege::Agent] the agent handling the run
13
13
  # @param message [Object] the inbound message being processed
14
14
  # @return [void]
15
- def initialize(persona:, message:)
15
+ def initialize(agent:, message:)
16
16
  @message = message
17
- super(persona:)
17
+ super(agent:)
18
18
  end
19
19
  end
20
20
  end
@@ -2,16 +2,16 @@
2
2
 
3
3
  module Protege
4
4
  module Orchestrator
5
- # The reactive run — answering one inbound email. Runs the receiving persona's +message_resolvers+
5
+ # The reactive run — answering one inbound email. Runs the receiving agent's +message_resolvers+
6
6
  # chain, opens the turn with the message body (plus any viewable attachments as vision parts), and
7
7
  # streams tokens when the message came from the console. The +Harness+ base owns the tool loop;
8
8
  # this subclass only supplies the reply-specific hooks.
9
9
  class ReplyHarness < Harness
10
- # @param persona [Protege::Persona] the persona that received the message
10
+ # @param agent [Protege::Agent] the agent that received the message
11
11
  # @param message [Object] the inbound message to respond to
12
12
  # @return [void]
13
- def initialize(persona:, message:)
14
- super(persona:)
13
+ def initialize(agent:, message:)
14
+ super(agent:)
15
15
  @message = message
16
16
  end
17
17
 
@@ -20,9 +20,9 @@ module Protege
20
20
  # @return [Object] the inbound message being answered (overrides the base nil)
21
21
  attr_reader :message
22
22
 
23
- # @return [Orchestrator::ResolverChain] the persona's reply chain
23
+ # @return [Orchestrator::ResolverChain] the agent's reply chain
24
24
  def resolvers
25
- persona.message_resolvers
25
+ agent.message_resolvers
26
26
  end
27
27
 
28
28
  # @return [String, Array<Object>] the message body, or body-plus-attachment parts
@@ -32,7 +32,7 @@ module Protege
32
32
 
33
33
  # @return [Orchestrator::ReplyContext] the shared per-run context carrying the message
34
34
  def context
35
- @context ||= ReplyContext.new(persona:, message:)
35
+ @context ||= ReplyContext.new(agent:, message:)
36
36
  end
37
37
 
38
38
  # Persist this tool-calling round as +ToolUse+ rows owned by the inbound message, so a later turn's
@@ -47,7 +47,7 @@ module Protege
47
47
  def persist_tool_round(turn:, response:, tool_results:)
48
48
  Protege::ToolUse.record_round(
49
49
  source: message,
50
- persona:,
50
+ agent:,
51
51
  run_id: Protege::Current.correlation_id,
52
52
  turn_index: turn,
53
53
  tool_calls: response.tool_calls,
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Protege
4
4
  module Orchestrator
5
- # Ordered list of resolver classes that build inference context for one persona. Configured at
6
- # persona declaration time (via +Persona::Config#resolvers+) and run once per
5
+ # Ordered list of resolver classes that build inference context for one agent. Configured at
6
+ # agent declaration time (via +Agent::Config#resolvers+) and run once per
7
7
  # +Harness#build_initial_request+ call.
8
8
  #
9
9
  # Each resolver runs independently and returns +Array<ModelMessage>+ or +nil+. The chain
@@ -18,7 +18,7 @@ module Protege
18
18
  #
19
19
  # @example
20
20
  # chain = ResolverChain.new
21
- # chain.use Protege::LoadFileResolver, 'app/personas/agent/PERSONA.md'
21
+ # chain.use Protege::LoadFileResolver, 'app/agents/executive/AGENT.md'
22
22
  # chain.use(Protege::LoadRecordResolver) { |ctx| Account.find_by(email: ctx.message.from_address) }
23
23
  # chain.use Protege::ThreadHistoryResolver
24
24
  #
@@ -4,18 +4,18 @@ module Protege
4
4
  module Orchestrator
5
5
  # Context for a proactive run — one driven by a scheduled +Responsibility+ (the Loop layer). Adds
6
6
  # the +responsibility+ to the shared +Context+ surface so tools and resolvers can reach the duty
7
- # being carried out (its prompt, schedule, owning persona). The +#message+ accessor inherited from
7
+ # being carried out (its prompt, schedule, owning agent). The +#message+ accessor inherited from
8
8
  # +Context+ stays nil, so reply-only tools (e.g. +send_email+ in reply mode) degrade gracefully.
9
9
  class ResponsibilityContext < Context
10
10
  # @return [Protege::Responsibility] the responsibility being fulfilled
11
11
  attr_reader :responsibility
12
12
 
13
- # @param persona [Protege::Persona] the persona carrying out the responsibility
13
+ # @param agent [Protege::Agent] the agent carrying out the responsibility
14
14
  # @param responsibility [Protege::Responsibility] the scheduled duty being run
15
15
  # @return [void]
16
- def initialize(persona:, responsibility:)
16
+ def initialize(agent:, responsibility:)
17
17
  @responsibility = responsibility
18
- super(persona:)
18
+ super(agent:)
19
19
  end
20
20
  end
21
21
  end
@@ -3,18 +3,18 @@
3
3
  module Protege
4
4
  module Orchestrator
5
5
  # The proactive run — carrying out one scheduled +Responsibility+ (the Loop layer). Runs the
6
- # persona's +responsibility_resolvers+ chain, which assembles the *entire* message list — including
6
+ # agent's +responsibility_resolvers+ chain, which assembles the *entire* message list — including
7
7
  # the responsibility's instructions (e.g. via +LoadTextResolver(role: :user) { ctx.responsibility.instructions }+).
8
8
  # The harness injects no user turn of its own (it inherits the base +user_content+ default of nil):
9
9
  # context assembly is the resolver chain's job. There is no inbound message and no streaming. The
10
10
  # +Harness+ base owns the tool loop; this subclass only supplies the responsibility-specific hooks.
11
11
  class ResponsibilityHarness < Harness
12
- # @param persona [Protege::Persona] the persona carrying out the responsibility
12
+ # @param agent [Protege::Agent] the agent carrying out the responsibility
13
13
  # @param responsibility [Protege::Responsibility] the scheduled duty to run
14
14
  # @param responsibility_run [Protege::ResponsibilityRun] the run record this execution belongs to
15
15
  # @return [void]
16
- def initialize(persona:, responsibility:, responsibility_run:)
17
- super(persona:)
16
+ def initialize(agent:, responsibility:, responsibility_run:)
17
+ super(agent:)
18
18
  @responsibility = responsibility
19
19
  @responsibility_run = responsibility_run
20
20
  end
@@ -27,14 +27,14 @@ module Protege
27
27
  # @return [Protege::ResponsibilityRun] the run record this execution belongs to
28
28
  attr_reader :responsibility_run
29
29
 
30
- # @return [Orchestrator::ResolverChain] the persona's scheduled-run chain
30
+ # @return [Orchestrator::ResolverChain] the agent's scheduled-run chain
31
31
  def resolvers
32
- persona.responsibility_resolvers
32
+ agent.responsibility_resolvers
33
33
  end
34
34
 
35
35
  # @return [Orchestrator::ResponsibilityContext] the shared per-run context carrying the responsibility
36
36
  def context
37
- @context ||= ResponsibilityContext.new(persona:, responsibility:)
37
+ @context ||= ResponsibilityContext.new(agent:, responsibility:)
38
38
  end
39
39
 
40
40
  # Persist this tool-calling round as +ToolUse+ rows owned by the +ResponsibilityRun+ — an audit
@@ -48,7 +48,7 @@ module Protege
48
48
  def persist_tool_round(turn:, response:, tool_results:)
49
49
  Protege::ToolUse.record_round(
50
50
  source: responsibility_run,
51
- persona:,
51
+ agent:,
52
52
  run_id: Protege::Current.correlation_id,
53
53
  turn_index: turn,
54
54
  tool_calls: response.tool_calls,
@@ -8,7 +8,7 @@ module Protege
8
8
  # The +Harness+ is this layer's primary construct: given one inbound message, it builds a provider
9
9
  # request, runs the multi-turn tool loop, and produces a final response. Supporting constructs in
10
10
  # this namespace include the +Context+ value objects passed to extensions, the +Tool+ and
11
- # +Resolver+ contracts, and the +ResolverChain+ that assembles inference context per persona.
11
+ # +Resolver+ contracts, and the +ResolverChain+ that assembles inference context per agent.
12
12
  #
13
13
  # Module methods here are the cross-namespace entry points. Callers (jobs, the +Extension+ tool mixin)
14
14
  # use these rather than reaching into +Harness+ subclasses or +ToolResult+/+Error+ directly.
@@ -17,21 +17,21 @@ module Protege
17
17
 
18
18
  # Run a reactive reply generation for an inbound message.
19
19
  #
20
- # @param persona [Protege::Persona] the receiving persona
20
+ # @param agent [Protege::Agent] the receiving agent
21
21
  # @param message [Protege::Message] the inbound message to answer
22
22
  # @return [Object] the harness result
23
- def reply(persona:, message:)
24
- ReplyHarness.run(persona:, message:)
23
+ def reply(agent:, message:)
24
+ ReplyHarness.run(agent:, message:)
25
25
  end
26
26
 
27
27
  # Run a proactive generation for a scheduled responsibility.
28
28
  #
29
- # @param persona [Protege::Persona] the persona carrying out the duty
29
+ # @param agent [Protege::Agent] the agent carrying out the duty
30
30
  # @param responsibility [Protege::Responsibility] the scheduled duty
31
31
  # @param responsibility_run [Protege::ResponsibilityRun] the run record being executed
32
32
  # @return [Object] the harness result
33
- def run_responsibility(persona:, responsibility:, responsibility_run:)
34
- ResponsibilityHarness.run(persona:, responsibility:, responsibility_run:)
33
+ def run_responsibility(agent:, responsibility:, responsibility_run:)
34
+ ResponsibilityHarness.run(agent:, responsibility:, responsibility_run:)
35
35
  end
36
36
  end
37
37
  end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protege
4
+ module Subscribers
5
+ # The operational-alert subscriber — emails the platform admin when an inference run fails. A peer of
6
+ # +Subscribers::Tracing+ / +HookDispatcher+ / +IntrospectionBroadcaster+, installed once at boot.
7
+ # Subscribes to +InferenceFailedEvent+ and, when a host has opted in via +config.failure_alerts+,
8
+ # sends a +Protege::AlertMailer+ notice carrying the error and the run's correlation id.
9
+ #
10
+ # Opt-in and best-effort by design:
11
+ #
12
+ # * Off until +config.failure_alerts[:to]+ is set, and it will not send without a +:from+ — there is
13
+ # no deliverable default sender (a real +:from+ must be a DKIM-signable address at a registered
14
+ # +EmailDomain+), so a +:to+ with no +:from+ is a misconfiguration that logs and sends nothing.
15
+ # * +InferenceFailedEvent+ fires once per failed *run*, and the job layer retries a transient failure
16
+ # up to three times — so a single failing message emits the event several times, all sharing one
17
+ # correlation id. Delivery is deduped on that id so the retry storm collapses to one alert, and a
18
+ # global per-window cap guards against a systemic outage flooding the admin.
19
+ # * The event is emitted from inside the Harness's rescue, right before it re-raises, and
20
+ # +ActiveSupport::Notifications+ propagates subscriber exceptions — so this module rescues
21
+ # everything internally. An alert failure must never mask the inference error that triggered it.
22
+ module Alerter
23
+ extend self
24
+
25
+ # How long a run's correlation id suppresses duplicate alerts (covers the retry backoff window).
26
+ DEDUP_TTL = 1.hour
27
+
28
+ # Cache key + ceiling + window for the systemic-outage flood guard.
29
+ GLOBAL_KEY = 'protege:alert:global'
30
+ GLOBAL_CAP = 10
31
+ GLOBAL_TTL = 1.hour
32
+
33
+ # Live subscription handles, retained so {reset!} can detach them.
34
+ @handles = []
35
+
36
+ # Subscribe the alerter to inference failures — the subscriber's +install!+ contract. Idempotent;
37
+ # called once at boot from the engine's +after_initialize+.
38
+ #
39
+ # @return [void]
40
+ def install!
41
+ return unless @handles.empty?
42
+
43
+ @handles = [InferenceFailedEvent.subscribe { |event| alert(event) }]
44
+ end
45
+
46
+ # Detach every subscription — the subscriber's +reset!+ contract, for boot re-wiring and tests.
47
+ #
48
+ # @return [void]
49
+ def reset!
50
+ @handles.each { |handle| ActiveSupport::Notifications.unsubscribe(handle) }
51
+ @handles = []
52
+ end
53
+
54
+ private
55
+
56
+ # Email the configured recipients about one failed run. Best-effort: gated on opt-in config,
57
+ # deduped, capped, and wrapped so any failure is logged and swallowed.
58
+ #
59
+ # @param event [Protege::InferenceFailedEvent] the failure event
60
+ # @return [void]
61
+ def alert(event)
62
+ return unless enabled?
63
+ return unless first_failure?(event)
64
+ return unless under_global_cap?
65
+
66
+ Protege::AlertMailer.inference_failed(
67
+ error_class: event.error.class.name,
68
+ error_message: event.error.message,
69
+ correlation_id: event.correlation_id
70
+ ).deliver_now
71
+ rescue StandardError => e
72
+ logger.error("[Protege] alert delivery failed: #{e.class}: #{e.message}")
73
+ end
74
+
75
+ # Whether alerting is configured to send. Requires recipients and a sender: without a deliverable
76
+ # +:from+ there is nothing safe to send from, so a +:to+ alone is a logged misconfiguration.
77
+ #
78
+ # @return [Boolean]
79
+ def enabled?
80
+ alerts = Protege.configuration.failure_alerts
81
+ return false if Array(alerts[:to]).reject(&:blank?).empty?
82
+
83
+ if alerts[:from].blank?
84
+ logger.warn('[Protege] failure_alerts :to is set but :from is missing; not sending ' \
85
+ '(a deliverable :from at a registered EmailDomain is required)')
86
+ return false
87
+ end
88
+
89
+ true
90
+ end
91
+
92
+ # Claim the first alert for this run's correlation id, so the job's retry storm sends only one.
93
+ # Returns true only for the first failure seen within {DEDUP_TTL}.
94
+ #
95
+ # @param event [Protege::InferenceFailedEvent] the failure event
96
+ # @return [Boolean]
97
+ def first_failure?(event)
98
+ Rails.cache.write("protege:alert:#{event.correlation_id}", true, expires_in: DEDUP_TTL, unless_exist: true)
99
+ end
100
+
101
+ # Whether this alert is within the global per-window cap. A best-effort read-modify-write (not
102
+ # atomic) — adequate as a flood guard, and store-agnostic (no reliance on +increment+ semantics).
103
+ #
104
+ # @return [Boolean]
105
+ def under_global_cap?
106
+ count = (Rails.cache.read(GLOBAL_KEY) || 0) + 1
107
+ Rails.cache.write(GLOBAL_KEY, count, expires_in: GLOBAL_TTL)
108
+ count <= GLOBAL_CAP
109
+ end
110
+
111
+ # @return [Logger] the engine logger
112
+ def logger
113
+ Protege.configuration.logger
114
+ end
115
+ end
116
+ end
117
+ end
@@ -14,7 +14,7 @@ module Protege
14
14
  #
15
15
  # A trace records only what affects training: the wire +request+ and +response+ (each already
16
16
  # serialized by the value object's own +#to_wire+, the same bytes the API exchange uses) and the
17
- # inference settings (+model+ + +settings+). No persona/provider identity is stored, and this module
17
+ # inference settings (+model+ + +settings+). No agent/provider identity is stored, and this module
18
18
  # does no serialization of its own — it just persists what the event carries.
19
19
  module Tracing
20
20
  extend self
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Protege
4
4
  # Current gem version, following Semantic Versioning.
5
- VERSION = '0.1.0.alpha.2'
5
+ VERSION = '0.1.0.alpha.7'
6
6
  end
data/lib/protege.rb CHANGED
@@ -9,12 +9,12 @@ require 'protege/engine'
9
9
  # Rails application into an agentic workspace.
10
10
  #
11
11
  # Protege is structured around the LOGI pipeline:
12
- # - **L**oop — scheduled responsibilities that drive personas on a cron tick.
12
+ # - **L**oop — scheduled responsibilities that drive agents on a cron tick.
13
13
  # - **O**rchestrator — the +Harness+ multi-turn tool loop that runs inference and dispatches tools.
14
14
  # - **G**ateway — Action Mailbox inbound delivery and SMTP outbound delivery.
15
15
  # - **I**nference — provider adapters that talk to model APIs.
16
16
  #
17
- # Email is the channel, the host Rails app is the workspace, and personas, tools, providers,
17
+ # Email is the channel, the host Rails app is the workspace, and agents, tools, providers,
18
18
  # resolvers, and hooks all participate as extensions. Requiring this file loads only the engine
19
19
  # entry points; runtime constructs are resolved through Rails autoloading once the engine boots.
20
20
  #
@@ -1,6 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  namespace :protege do
4
+ namespace :toolkits do
5
+ desc 'Ensure the system toolkits (All Tools / Default Tools) exist and refresh All Tools to the ' \
6
+ 'full tool registry. Idempotent — run on each deploy (e.g. after db:migrate).'
7
+ task sync: :environment do
8
+ # Rake tasks don't eager-load by default, so the tool manifest would be incomplete; force it so
9
+ # All Tools captures every registered tool.
10
+ Rails.application.eager_load!
11
+ Protege::SystemToolkits.sync!
12
+ puts "Synced system toolkits — All Tools has #{Protege::Toolkit.all_tools&.member_tool_ids&.size} tools."
13
+ end
14
+ end
15
+
4
16
  namespace :css do
5
17
  desc "Build the Protege engine's compiled Tailwind CSS. " \
6
18
  'Run this after changing engine views. Requires tailwindcss-rails in the host app.'