protege 0.1.0.alpha.2 → 0.1.0.alpha.8

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 (181) 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/dev_setup.rb +110 -0
  55. data/app/services/protege/message_search.rb +2 -2
  56. data/app/services/protege/system_toolkits.rb +80 -0
  57. data/app/tools/protege/create_file_tool.rb +2 -0
  58. data/app/tools/protege/read_attachment_tool.rb +3 -1
  59. data/app/tools/protege/search_emails_tool.rb +15 -13
  60. data/app/tools/protege/send_email_tool.rb +60 -18
  61. data/app/tools/protege/web_fetch_tool.rb +2 -0
  62. data/app/tools/protege/web_search_tool.rb +3 -1
  63. data/app/views/layouts/mailer.text.erb +1 -0
  64. data/app/views/protege/access_rules/create.turbo_stream.slim +3 -3
  65. data/app/views/protege/agent_toolkit_rules/create.turbo_stream.slim +6 -0
  66. data/app/views/protege/agent_toolkits/_gate_rule.html.slim +7 -0
  67. data/app/views/protege/agent_toolkits/_gate_rule_form.html.slim +8 -0
  68. data/app/views/protege/agent_toolkits/_gate_rules.html.slim +19 -0
  69. data/app/views/protege/agent_toolkits/show.html.slim +17 -0
  70. data/app/views/protege/{personas → agents}/_access_rule.html.slim +1 -1
  71. data/app/views/protege/{personas → agents}/_access_rule_form.html.slim +1 -1
  72. data/app/views/protege/{personas → agents}/_access_rules.html.slim +5 -5
  73. data/app/views/protege/agents/_actions.html.slim +15 -0
  74. data/app/views/protege/agents/_agent_sidebar_item.html.slim +5 -0
  75. data/app/views/protege/agents/_agent_toolkit.html.slim +7 -0
  76. data/app/views/protege/agents/_agent_toolkit_form.html.slim +7 -0
  77. data/app/views/protege/agents/_form.html.slim +25 -0
  78. data/app/views/protege/agents/_sidebar.html.slim +11 -0
  79. data/app/views/protege/agents/_toolkits.html.slim +19 -0
  80. data/app/views/protege/agents/agent_toolkits/create.turbo_stream.slim +6 -0
  81. data/app/views/protege/agents/edit.html.slim +7 -0
  82. data/app/views/protege/agents/index.html.slim +6 -0
  83. data/app/views/protege/agents/new.html.slim +7 -0
  84. data/app/views/protege/agents/show.html.slim +29 -0
  85. data/app/views/protege/alert_mailer/inference_failed.text.erb +4 -0
  86. data/app/views/protege/email_domains/show.html.slim +1 -1
  87. data/app/views/protege/home/show.html.slim +13 -13
  88. data/app/views/protege/responsibilities/_form.html.slim +2 -2
  89. data/app/views/protege/responsibilities/_responsibility_sidebar_item.html.slim +1 -1
  90. data/app/views/protege/responsibilities/new.html.slim +1 -1
  91. data/app/views/protege/responsibilities/show.html.slim +2 -2
  92. data/app/views/protege/responsibility_runs/show.html.slim +1 -1
  93. data/app/views/protege/shared/_header.html.slim +2 -1
  94. data/app/views/protege/shared/_hotkeys.html.slim +15 -11
  95. data/app/views/protege/threads/new.html.slim +2 -2
  96. data/app/views/protege/toolkits/_form.html.slim +13 -0
  97. data/app/views/protege/toolkits/_sidebar.html.slim +11 -0
  98. data/app/views/protege/toolkits/_toolkit_sidebar_item.html.slim +5 -0
  99. data/app/views/protege/toolkits/edit.html.slim +7 -0
  100. data/app/views/protege/toolkits/index.html.slim +6 -0
  101. data/app/views/protege/toolkits/new.html.slim +7 -0
  102. data/app/views/protege/toolkits/show.html.slim +13 -0
  103. data/config/routes.rb +12 -1
  104. data/db/migrate/20260707120000_replace_persona_active_with_archived_at.rb +1 -1
  105. data/db/migrate/20260708120000_add_disabled_tool_ids_to_personas.rb +1 -1
  106. data/db/migrate/20260728140000_scope_message_and_thread_uniqueness_per_persona.rb +16 -0
  107. data/db/migrate/20260812000001_create_protege_toolkits.rb +37 -0
  108. data/db/migrate/20260815000001_remove_disabled_tool_ids_from_personas.rb +21 -0
  109. data/db/migrate/20260815000002_add_key_to_protege_toolkits.rb +22 -0
  110. data/db/migrate/20260815100000_rename_personas_to_agents.rb +24 -0
  111. data/lib/generators/protege/agent/agent_generator.rb +35 -0
  112. data/lib/generators/protege/{persona/templates/persona.rb.tt → agent/templates/agent.rb.tt} +9 -9
  113. data/lib/generators/protege/extension_naming.rb +1 -1
  114. data/lib/generators/protege/hook/templates/hook.rb.tt +2 -2
  115. data/lib/generators/protege/install/install_generator.rb +19 -11
  116. data/lib/generators/protege/install/templates/initializer.rb.tt +17 -6
  117. data/lib/generators/protege/postfix/templates/deploy/mail/MAIL.md +2 -2
  118. data/lib/generators/protege/resolver/resolver_generator.rb +2 -2
  119. data/lib/generators/protege/resolver/templates/resolver.rb.tt +4 -4
  120. data/lib/generators/protege/tool/templates/tool.rb.tt +7 -5
  121. data/lib/protege/configuration.rb +43 -8
  122. data/lib/protege/engine.rb +3 -1
  123. data/lib/protege/errors/tool_not_available_error.rb +7 -6
  124. data/lib/protege/events/event.rb +2 -2
  125. data/lib/protege/events/inference_chunk_event.rb +1 -1
  126. data/lib/protege/events/inference_completed_event.rb +1 -1
  127. data/lib/protege/events/inference_failed_event.rb +1 -1
  128. data/lib/protege/events/inference_generated_event.rb +1 -1
  129. data/lib/protege/events/inference_max_turns_reached_event.rb +1 -1
  130. data/lib/protege/events/inference_started_event.rb +1 -1
  131. data/lib/protege/events/loop_run_completed_event.rb +1 -1
  132. data/lib/protege/events/loop_run_enqueued_event.rb +1 -1
  133. data/lib/protege/events/loop_run_failed_event.rb +1 -1
  134. data/lib/protege/events/loop_run_started_event.rb +1 -1
  135. data/lib/protege/events/tool_call_completed_event.rb +1 -1
  136. data/lib/protege/events/tool_call_failed_event.rb +1 -1
  137. data/lib/protege/events/tool_call_started_event.rb +1 -1
  138. data/lib/protege/events/tool_calls_received_event.rb +1 -1
  139. data/lib/protege/extensions/hook.rb +1 -0
  140. data/lib/protege/extensions/hook_mixin.rb +1 -1
  141. data/lib/protege/extensions/manifest.rb +67 -0
  142. data/lib/protege/extensions/manifested.rb +81 -0
  143. data/lib/protege/extensions/provider.rb +1 -0
  144. data/lib/protege/extensions/provider_mixin.rb +9 -17
  145. data/lib/protege/extensions/resolver.rb +2 -1
  146. data/lib/protege/extensions/resolver_mixin.rb +2 -2
  147. data/lib/protege/extensions/tool.rb +1 -0
  148. data/lib/protege/extensions/tool_mixin.rb +16 -40
  149. data/lib/protege/gateway/access_control.rb +19 -19
  150. data/lib/protege/gateway/access_policy.rb +2 -2
  151. data/lib/protege/gateway/mail/outbound.rb +22 -5
  152. data/lib/protege/gateway.rb +48 -37
  153. data/lib/protege/loop/scheduler.rb +1 -1
  154. data/lib/protege/orchestrator/context.rb +8 -8
  155. data/lib/protege/orchestrator/harness.rb +26 -24
  156. data/lib/protege/orchestrator/reply_context.rb +3 -3
  157. data/lib/protege/orchestrator/reply_harness.rb +8 -8
  158. data/lib/protege/orchestrator/resolver_chain.rb +3 -3
  159. data/lib/protege/orchestrator/responsibility_context.rb +4 -4
  160. data/lib/protege/orchestrator/responsibility_harness.rb +8 -8
  161. data/lib/protege/orchestrator.rb +7 -7
  162. data/lib/protege/subscribers/alerter.rb +117 -0
  163. data/lib/protege/subscribers/tracing.rb +1 -1
  164. data/lib/protege/version.rb +1 -1
  165. data/lib/protege.rb +2 -2
  166. data/lib/tasks/protege_tasks.rake +40 -0
  167. metadata +67 -23
  168. data/app/controllers/concerns/protege/persona_scoped.rb +0 -22
  169. data/app/controllers/protege/personas_controller.rb +0 -118
  170. data/app/helpers/protege/personas_helper.rb +0 -90
  171. data/app/models/concerns/protege/broadcastable_persona.rb +0 -50
  172. data/app/models/concerns/protege/tool_scoped.rb +0 -88
  173. data/app/views/protege/personas/_actions.html.slim +0 -15
  174. data/app/views/protege/personas/_form.html.slim +0 -29
  175. data/app/views/protege/personas/_persona_sidebar_item.html.slim +0 -5
  176. data/app/views/protege/personas/_sidebar.html.slim +0 -11
  177. data/app/views/protege/personas/edit.html.slim +0 -7
  178. data/app/views/protege/personas/index.html.slim +0 -6
  179. data/app/views/protege/personas/new.html.slim +0 -7
  180. data/app/views/protege/personas/show.html.slim +0 -37
  181. data/lib/generators/protege/persona/persona_generator.rb +0 -35
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rails/generators/base'
4
- require_relative '../persona/persona_generator'
4
+ require_relative '../agent/agent_generator'
5
5
 
6
6
  module Protege
7
7
  module Generators
8
8
  # Bootstraps Protege into a host Rails app — the one command to go from +gem 'protege'+ to a
9
9
  # runnable dashboard. It installs the Rails frameworks Protege depends on (Active Storage for
10
10
  # attachments, Action Mailbox for inbound mail), writes a fully-documented initializer, mounts the
11
- # engine, wires the Loop's recurring tick, and scaffolds a starter persona. The steps it cannot do
11
+ # engine, wires the Loop's recurring tick, and scaffolds a starter agent. The steps it cannot do
12
12
  # safely for you — wrapping the mount in your own authentication, running migrations, setting
13
13
  # credentials — are printed as next steps.
14
14
  #
@@ -17,7 +17,7 @@ module Protege
17
17
  class InstallGenerator < Rails::Generators::Base
18
18
  source_root File.expand_path('templates', __dir__)
19
19
 
20
- desc 'Install Protege into the host app: initializer, engine mount, recurring tick, and a starter persona.'
20
+ desc 'Install Protege into the host app: initializer, engine mount, recurring tick, and a starter agent.'
21
21
 
22
22
  # The recurring job entry that drives the Loop scheduler — one tick a minute fires due
23
23
  # responsibilities. Written at column zero; {#indented_tick} nests it under an environment key.
@@ -76,12 +76,12 @@ module Protege
76
76
  end
77
77
  end
78
78
 
79
- # Scaffold a starter +Agent+ persona by delegating to the persona generator (single source of
80
- # truth for the persona template).
79
+ # Scaffold a starter +ExecutiveAgent+ by delegating to the agent generator (single source of
80
+ # truth for the agent template).
81
81
  #
82
82
  # @return [void]
83
- def create_starter_persona
84
- invoke Protege::Generators::PersonaGenerator, ['Agent']
83
+ def create_starter_agent
84
+ invoke Protege::Generators::AgentGenerator, ['Executive']
85
85
  end
86
86
 
87
87
  # Print the wiring the generator can't safely automate.
@@ -96,19 +96,27 @@ module Protege
96
96
  1. Run the migrations (Active Storage, Action Mailbox, and Protege's own):
97
97
  bin/rails db:migrate
98
98
 
99
- 2. WRAP THE ENGINE MOUNT in your authentication in config/routes.rb — the engine ships no
99
+ 2. Sync the system toolkits (creates "All Tools" and "Default Tools"; agents you create
100
+ afterwards auto-attach Default Tools, so they can reply out of the box). Run it on
101
+ every deploy, after db:migrate:
102
+ bin/rails protege:toolkits:sync
103
+
104
+ 3. WRAP THE ENGINE MOUNT in your authentication in config/routes.rb — the engine ships no
100
105
  auth of its own. An unwrapped mount exposes the dashboard to anyone.
101
106
 
102
- 3. Set your provider credentials (the defaults read OpenRouter from ENV):
107
+ 4. Set your provider credentials (the defaults read OpenRouter from ENV):
103
108
  OPENROUTER_API_KEY=sk-... (and OPENROUTER_MODEL to pick a model)
104
109
 
105
- 4. Let extensions register in development by loading them eagerly — add to
110
+ 5. Let extensions register in development by loading them eagerly — add to
106
111
  config/environments/development.rb:
107
112
  config.eager_load = true
108
113
  (tools/providers/hooks are discovered as loaded subclasses).
109
114
 
110
- 5. Create your first persona + email domain (dashboard, or a db/seeds.rb bootstrap) so
115
+ 6. Create your first agent + email domain (dashboard, or a db/seeds.rb bootstrap) so
111
116
  inbound mail has somewhere to route. Then boot and open /protege.
117
+
118
+ Or, for a ready-to-chat development setup in one command (covers steps 2 and 6):
119
+ bin/rails protege:setup:dev
112
120
  STEPS
113
121
  end
114
122
 
@@ -16,8 +16,13 @@ Protege.configure do |config|
16
16
 
17
17
  # ── Inference ─────────────────────────────────────────────────────────────────
18
18
 
19
- # Maximum tool-calling rounds before the harness returns the last response. (default: 8)
20
- # config.max_tool_turns = 8
19
+ # Maximum tool-calling rounds before the harness returns the last response. (default: 100)
20
+ # config.max_tool_turns = 100
21
+
22
+ # X-Protege-Recursion hop count at which inbound mail is silently dropped — bounds agent-to-agent
23
+ # reply loops. A human replying anywhere resets the chain (mail clients don't echo the header).
24
+ # (default: 50)
25
+ # config.recursion_limit = 50
21
26
 
22
27
  # Symbolic id of the provider extension to run inference through; must match a registered provider's
23
28
  # +protege_id+ (here the built-in OpenRouter's :openrouter). (default: nil)
@@ -47,8 +52,8 @@ Protege.configure do |config|
47
52
 
48
53
  # ── Inbound access control ──────────────────────────────────────────────────
49
54
 
50
- # Global ceiling on which senders may reach ANY persona — the committed layer of the access guardrail.
51
- # Per-persona rules (managed in the dashboard) only narrow this further, never widen it. Patterns are
55
+ # Global ceiling on which senders may reach ANY agent — the committed layer of the access guardrail.
56
+ # Per-agent rules (managed in the dashboard) only narrow this further, never widen it. Patterns are
52
57
  # an exact address or a single "*" wildcard. Defaults to permit-all.
53
58
  # config.inbound_access = Protege::Gateway.build_access_policy(allow: ['*@your-company.com'])
54
59
  # config.inbound_access = Protege::Gateway.build_access_policy(deny: ['*@spam.example'])
@@ -68,15 +73,21 @@ Protege.configure do |config|
68
73
 
69
74
  # ── Extension scaffold paths ──────────────────────────────────────────────────
70
75
 
71
- # Where the `bin/rails g protege:{tool,resolver,hook,persona,provider}` generators write. Defaults to
76
+ # Where the `bin/rails g protege:{tool,resolver,hook,agent,provider}` generators write. Defaults to
72
77
  # the conventional app/ directories; override if you group extensions elsewhere (must stay under an
73
78
  # autoloaded path).
74
79
  # config.tools_path = 'app/tools'
75
80
  # config.resolvers_path = 'app/resolvers'
76
81
  # config.hooks_path = 'app/hooks'
77
- # config.personas_path = 'app/personas'
82
+ # config.agents_path = 'app/agents'
78
83
  # config.providers_path = 'app/providers'
79
84
 
85
+ # ── Failure alerts ──────────────────────────────────────────────────────────
86
+
87
+ # Email the platform operator when an inference run fails (sent via Action Mailer, outside the
88
+ # agent's own mail flow). Off by default; no alert is sent until `to:` is set.
89
+ # config.failure_alerts = { to: ['ops@example.com'], from: 'alerts@your-domain.com' }
90
+
80
91
  # ── Logging ─────────────────────────────────────────────────────────────────
81
92
 
82
93
  # Logger for engine output; defaults to Rails.logger. Override to route to a dedicated target:
@@ -101,7 +101,7 @@ Pre-stage the TXT records anytime. Do **not** change the MX until you cut over (
101
101
  2. Deploy the app and **boot the accessory** (Kamal `deploy` does not manage accessories):
102
102
  `kamal accessory boot mail`.
103
103
  3. Point the **MX** at `agent.example.com` (priority 10). Inbound now arrives at your box.
104
- 4. Validate: `kamal accessory logs mail`; email the persona from Gmail → reply lands with
104
+ 4. Validate: `kamal accessory logs mail`; email the agent from Gmail → reply lands with
105
105
  **SPF/DKIM/DMARC = pass** ("Show original"). Score at [mail-tester.com](https://www.mail-tester.com)
106
106
  and iterate on reputation.
107
107
 
@@ -126,7 +126,7 @@ Our own outbound (the app container, on RFC1918) skips all three — only remote
126
126
  first-run window: deploy with `monitor`, watch `kamal accessory logs mail` for `dmarc=fail` on
127
127
  legitimate senders, then switch to `enforce` and redeploy (no image rebuild needed).
128
128
 
129
- To verify: from a Gmail account, email the persona and confirm a normal message is delivered with
129
+ To verify: from a Gmail account, email the agent and confirm a normal message is delivered with
130
130
  `dmarc=pass`; a spoofing attempt against a `p=reject` domain is refused at SMTP (visible in the logs).
131
131
 
132
132
  ## Rollback
@@ -5,12 +5,12 @@ require_relative '../extension_naming'
5
5
 
6
6
  module Protege
7
7
  module Generators
8
- # Scaffolds a Protege resolver — one link in a persona's context-building chain — into the host
8
+ # Scaffolds a Protege resolver — one link in an agent's context-building chain — into the host
9
9
  # app's resolvers directory (+config.resolvers_path+, default +app/resolvers+). A resolver
10
10
  # subclasses +Protege::Resolver+ and implements +resolve+, contributing zero or more
11
11
  # +Protege::ModelMessage+s to the prompt the harness sends the model.
12
12
  #
13
- # Unlike tools, resolvers are NOT auto-discovered: add the class to a persona's chain
13
+ # Unlike tools, resolvers are NOT auto-discovered: add the class to an agent's chain
14
14
  # (+chain.use MyResolver+) for it to run. The +Resolver+ suffix is applied for you.
15
15
  #
16
16
  # @example
@@ -2,10 +2,10 @@
2
2
 
3
3
  # <%= extension_class_name('Resolver') %> — contributes context to the prompt before the model runs.
4
4
  #
5
- # A resolver is one link in a persona's chain. Subclass +Protege::Resolver+, implement +#resolve+, and
5
+ # A resolver is one link in an agent's chain. Subclass +Protege::Resolver+, implement +#resolve+, and
6
6
  # add it to a chain to activate it (resolvers are NOT auto-discovered):
7
7
  #
8
- # # in app/personas/your_persona.rb
8
+ # # in app/agents/your_agent.rb
9
9
  # message_resolvers { |chain| chain.use <%= extension_class_name('Resolver') %> } # reply runs
10
10
  # responsibility_resolvers { |chain| chain.use <%= extension_class_name('Resolver') %> } # scheduled runs
11
11
  #
@@ -17,7 +17,7 @@ class <%= extension_class_name('Resolver') %> < Protege::Resolver
17
17
  # The +context+ tells you which kind of run this is (read it, never mutate it — it is frozen):
18
18
  # - reply run: +context.message+ is the inbound +Protege::Message+ (and +context.message.email_thread+)
19
19
  # - scheduled run: +context.responsibility+ is the +Protege::Responsibility+; +context.message+ is nil
20
- # Both always expose +context.persona+, +context.config+, and +context.logger+.
20
+ # Both always expose +context.agent+, +context.config+, and +context.logger+.
21
21
  #
22
22
  # Pick a +role+ for what you contribute: +:system+ for standing instructions/facts, +:user+ or
23
23
  # +:assistant+ for conversation turns, +:tool+ for a replayed tool result.
@@ -26,6 +26,6 @@ class <%= extension_class_name('Resolver') %> < Protege::Resolver
26
26
  # @return [Protege::ModelMessage, Array<Protege::ModelMessage>, nil] the contribution, or nil for none
27
27
  def resolve(context:)
28
28
  # TODO: build and return your context. Return nil when there is nothing to add.
29
- message(role: :system, content: "TODO: context for #{context.persona.name}")
29
+ message(role: :system, content: "TODO: context for #{context.agent.name}")
30
30
  end
31
31
  end
@@ -2,9 +2,11 @@
2
2
 
3
3
  # <%= extension_class_name('Tool') %> — a capability the agent can call mid-conversation.
4
4
  #
5
- # Subclassing +Protege::Tool+ registers this tool automatically (no wiring): the harness publishes it
6
- # in the model's tool catalog and routes matching tool calls to +#use+. The tool's id is derived from
7
- # this class name with the +Tool+ suffix stripped and snake_cased — here +:<%= extension_file_name('Tool').sub(/_tool\z/, '') %>+.
5
+ # Subclassing +Protege::Tool+ registers this tool automatically in the manifest; an agent can use it
6
+ # once it belongs to a toolkit attached to that agent (add it in the dashboard, or run
7
+ # `bin/rails protege:toolkits:sync` so the All Tools toolkit picks it up). When offered, the harness
8
+ # routes matching tool calls to +#use+. The tool's id is derived from this class name with the +Tool+
9
+ # suffix stripped and snake_cased — here +:<%= extension_file_name('Tool').sub(/_tool\z/, '') %>+.
8
10
  #
9
11
  # Fill in the three things the model relies on:
10
12
  # 1. +description+ — WHEN to reach for this tool and WHAT it does, written for the model to read.
@@ -40,11 +42,11 @@ class <%= extension_class_name('Tool') %> < Protege::Tool
40
42
  # the model as a failure, so raise freely for the genuinely exceptional.
41
43
  #
42
44
  # @param context [Protege::Orchestrator::Context] the run context — +context.message+ on a reply run
43
- # (and its +email_thread+), +context.responsibility+ on a scheduled run, plus +persona+/+logger+
45
+ # (and its +email_thread+), +context.responsibility+ on a scheduled run, plus +agent+/+logger+
44
46
  # @param example [String] the example argument declared in {input_schema} (rename to your own)
45
47
  # @return [Protege::Result] the outcome handed back to the model
46
48
  def use(context:, example:)
47
- # TODO: implement. `context` gives you the persona and the current message/thread or responsibility.
49
+ # TODO: implement. `context` gives you the agent and the current message/thread or responsibility.
48
50
  success(echo: example)
49
51
  end
50
52
  end
@@ -30,6 +30,13 @@ module Protege
30
30
  # Maximum tool-calling rounds before returning the last response.
31
31
  attr_accessor :max_tool_turns
32
32
 
33
+ # The +X-Protege-Recursion+ hop count at which inbound mail is silently dropped — the bound on
34
+ # agent-to-agent reply loops (see +Gateway::RECURSION_HEADER+). Agent-sent mail stamps the header
35
+ # (+1+ fresh, inbound plus one on a reply); a human replying anywhere resets the chain, since mail
36
+ # clients never echo the header back. Defaults to 50 — roomy enough for long agent-to-agent
37
+ # hand-offs while still bounding a true infinite loop.
38
+ attr_accessor :recursion_limit
39
+
33
40
  # Email address representing the dashboard user in local conversations. Messages from the
34
41
  # dashboard use this as their +from_address+, and outbound replies to this address skip SMTP
35
42
  # delivery.
@@ -50,11 +57,11 @@ module Protege
50
57
  attr_writer :logger
51
58
 
52
59
  # Destination directories the extension scaffold generators (+protege:tool+, +:resolver+, +:hook+,
53
- # +:persona+, +:provider+) write into, each relative to the app root. Defaults to the conventional
54
- # +app/tools+, +app/resolvers+, +app/hooks+, +app/personas+, +app/providers+. Override in the
60
+ # +:agent+, +:provider+) write into, each relative to the app root. Defaults to the conventional
61
+ # +app/tools+, +app/resolvers+, +app/hooks+, +app/agents+, +app/providers+. Override in the
55
62
  # initializer if a host groups its extensions elsewhere (e.g. +config.tools_path = 'app/agents/tools'+);
56
63
  # the target must stay under an autoloaded path so Rails still loads the extension.
57
- attr_accessor :tools_path, :resolvers_path, :hooks_path, :personas_path, :providers_path
64
+ attr_accessor :tools_path, :resolvers_path, :hooks_path, :agents_path, :providers_path
58
65
 
59
66
  # Per-provider options, keyed by provider id — assign a plain Hash (see {#providers}).
60
67
  attr_writer :providers
@@ -69,6 +76,11 @@ module Protege
69
76
  # {#tracing}). Off by default; nothing is captured until a host opts in.
70
77
  attr_writer :tracing
71
78
 
79
+ # Operational failure-alert options — assign a plain Hash, e.g.
80
+ # +config.failure_alerts = { to: ['ops@you.com'], from: 'alerts@your-domain.com' }+ (see
81
+ # {#failure_alerts}). Off by default; no alert mail is sent until a host sets +:to+.
82
+ attr_writer :failure_alerts
83
+
72
84
  # Initialize configuration with engine defaults.
73
85
  #
74
86
  # @return [void]
@@ -76,18 +88,20 @@ module Protege
76
88
  @nav_title = '🥚 Protege'
77
89
  @console_address = 'console@protege.local'
78
90
  @http_user_agent = DEFAULT_HTTP_USER_AGENT
79
- @max_tool_turns = 8
91
+ @max_tool_turns = 100
92
+ @recursion_limit = 50
80
93
  @provider_id = nil
81
94
  @logger = nil
82
95
  @tools_path = 'app/tools'
83
96
  @resolvers_path = 'app/resolvers'
84
97
  @hooks_path = 'app/hooks'
85
- @personas_path = 'app/personas'
98
+ @agents_path = 'app/agents'
86
99
  @providers_path = 'app/providers'
87
100
  @providers = nil
88
101
  @attachment_policy = nil
89
102
  @inbound_access = nil
90
103
  @tracing = nil
104
+ @failure_alerts = nil
91
105
  end
92
106
 
93
107
  # Return the configured logger, lazily resolving a default.
@@ -139,14 +153,14 @@ module Protege
139
153
  end
140
154
 
141
155
  # The global inbound access policy — the committed, org-wide ceiling on which senders may reach
142
- # *any* persona. This is the static layer of the access-control guardrail; the runtime,
143
- # per-persona layer lives in +Protege::AccessRule+ records, and +AccessControl+ intersects the two
156
+ # *any* agent. This is the static layer of the access-control guardrail; the runtime,
157
+ # per-agent layer lives in +Protege::AccessRule+ records, and +AccessControl+ intersects the two
144
158
  # (each layer can only narrow, never widen).
145
159
  #
146
160
  # Defaults to a bare permit-everyone policy, built lazily on first read so an unconfigured engine
147
161
  # imposes no constraint.
148
162
  #
149
- # @example Restrict every persona to the company domain
163
+ # @example Restrict every agent to the company domain
150
164
  # config.inbound_access = Protege::Gateway.build_access_policy(allow: ['*@company.co'])
151
165
  #
152
166
  # @return [Protege::Gateway::AccessPolicy] the configured policy, or a permit-everyone default
@@ -167,5 +181,26 @@ module Protege
167
181
  def tracing
168
182
  @tracing ||= { enabled: false }
169
183
  end
184
+
185
+ # Operational failure-alert options, a plain Hash — the opt-in seam for emailing a platform admin
186
+ # when inference fails. Assigned wholesale in the initializer (like {#tracing}); assignment replaces
187
+ # the whole Hash. Recognised keys:
188
+ #
189
+ # * +:to+ — an Array of recipient addresses. Alerting is enabled only when this is present and
190
+ # non-empty, so an unconfigured engine (the default +{}+) sends nothing.
191
+ # * +:from+ — the sender address. For production deliverability this must be an address at a
192
+ # registered +EmailDomain+ so the self-hosted MTA DKIM-signs it and it passes that domain's DMARC
193
+ # policy; there is no safe way to deduce it, so it is set explicitly. Omitted → the
194
+ # +ApplicationMailer+ default sender (adequate only under the +:test+ delivery method).
195
+ #
196
+ # When +:to+ is set, +Protege::Subscribers::Alerter+ emails those recipients on inference failure.
197
+ #
198
+ # @example Turn failure alerts on
199
+ # config.failure_alerts = { to: ['ops@you.com'], from: 'alerts@agent.you.com' }
200
+ #
201
+ # @return [Hash] the failure-alert options, defaulting to +{}+ (disabled)
202
+ def failure_alerts
203
+ @failure_alerts ||= {}
204
+ end
170
205
  end
171
206
  end
@@ -83,11 +83,13 @@ module Protege
83
83
  # configuration, and installs the engine's event subscribers (all under +Protege::Subscribers+): the
84
84
  # introspection broadcaster (console activity feed), the hook dispatcher (fans events to host hooks —
85
85
  # its dispatch reads +HookDispatcher+'s registry live, so reloaded hooks are picked up without
86
- # re-subscribing), and tracing. Each exposes the +install!+ contract and subscribes once here.
86
+ # re-subscribing), tracing, and the alerter (emails the platform admin on inference failure when
87
+ # +config.failure_alerts+ opts in). Each exposes the +install!+ contract and subscribes once here.
87
88
  config.after_initialize do
88
89
  Protege::IntrospectionBroadcaster.install!
89
90
  Protege::Subscribers::HookDispatcher.install!
90
91
  Protege::Subscribers::Tracing.install!
92
+ Protege::Subscribers::Alerter.install!
91
93
  end
92
94
 
93
95
  # Return the absolute path to a file in the engine's lib/ directory. Used by the engine's rake tasks
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Protege
4
- # The model called a registered tool that is not in the current persona's scope — its code grant
5
- # minus +disabled_tool_ids+ (see +Protege::ToolScoped+). Distinct from +ToolNotFoundError+ (the tool
6
- # does not exist at all): here the tool exists but is withheld from this persona, so dispatch refuses
7
- # it before +#use+ runs and the model gets a serialized failure to self-correct from. Guards the case
8
- # where a tool disabled mid-thread is still visible to the model in earlier turns.
4
+ # The model called a registered tool that is not in the current agent's scope for this run — the tools
5
+ # its attached toolkits expose to the run's sender (see +Protege::Toolable+ / +AgentToolkit+). Distinct
6
+ # from +ToolNotFoundError+ (the tool does not exist at all): here the tool exists but is withheld from
7
+ # this agent, so dispatch refuses it before +#use+ runs and the model gets a serialized failure to
8
+ # self-correct from. Guards the case where a tool that left scope mid-thread is still visible to the
9
+ # model in earlier turns.
9
10
  class ToolNotAvailableError < Error
10
- def initialize(name:) = super("tool :#{name} is not available to this persona")
11
+ def initialize(name:) = super("tool :#{name} is not available to this agent")
11
12
  end
12
13
  end
@@ -10,12 +10,12 @@ module Protege
10
10
  # same class-attribute style as a provider's +protege_id+) and writes a plain reader per payload key.
11
11
  # Publishing and subscribing go through the subclass:
12
12
  #
13
- # Protege::InferenceCompletedEvent.emit(persona:, message:, result:) # engine internals
13
+ # Protege::InferenceCompletedEvent.emit(agent:, message:, result:) # engine internals
14
14
  # Protege::InferenceCompletedEvent.subscribe { |event| event.result } # host / broadcaster
15
15
  #
16
16
  # +correlation_id+ is injected automatically from +Protege::Current+ into every emitted event — set it
17
17
  # once in +AgentMailbox+ and it rides through the whole processing run. A subscriber's block receives a
18
- # frozen instance of the specific event class, so +event.persona+ / +event.result+ read the payload
18
+ # frozen instance of the specific event class, so +event.agent+ / +event.result+ read the payload
19
19
  # with named, nil-safe accessors (and +event[:key]+ reaches anything without a dedicated reader).
20
20
  class Event
21
21
  # @return [Hash] the underlying payload hash
@@ -5,7 +5,7 @@ module Protege
5
5
  class InferenceChunkEvent < Event
6
6
  channel 'protege.inference.chunk'
7
7
 
8
- def persona = self[:persona]
8
+ def agent = self[:agent]
9
9
  def thread = self[:thread]
10
10
  def chunk = self[:chunk]
11
11
 
@@ -5,7 +5,7 @@ module Protege
5
5
  class InferenceCompletedEvent < Event
6
6
  channel 'protege.inference.completed'
7
7
 
8
- def persona = self[:persona]
8
+ def agent = self[:agent]
9
9
  def message = self[:message]
10
10
  def result = self[:result]
11
11
  end
@@ -5,7 +5,7 @@ module Protege
5
5
  class InferenceFailedEvent < Event
6
6
  channel 'protege.inference.failed'
7
7
 
8
- def persona = self[:persona]
8
+ def agent = self[:agent]
9
9
  def message = self[:message]
10
10
  def error = self[:error]
11
11
  end
@@ -5,7 +5,7 @@ module Protege
5
5
  # final tool-free turn. Unlike +InferenceCompletedEvent+ (one per run, carrying only the final
6
6
  # result), this carries the wire +request+ as sent, the +response+ it produced, and the inference
7
7
  # settings (+model+ + +settings+) that shaped it — everything a trace needs to be a training example,
8
- # and nothing else (no persona/provider identity). This is the seam the tracing subscriber listens on;
8
+ # and nothing else (no agent/provider identity). This is the seam the tracing subscriber listens on;
9
9
  # +correlation_id+ (from +Protege::Current+) and +turn+ group the turns of one run for dedup.
10
10
  class InferenceGeneratedEvent < Event
11
11
  channel 'protege.inference.generated'
@@ -5,7 +5,7 @@ module Protege
5
5
  class InferenceMaxTurnsReachedEvent < Event
6
6
  channel 'protege.inference.max_turns_reached'
7
7
 
8
- def persona = self[:persona]
8
+ def agent = self[:agent]
9
9
  def turn = self[:turn]
10
10
  def tool_calls = self[:tool_calls]
11
11
  end
@@ -5,7 +5,7 @@ module Protege
5
5
  class InferenceStartedEvent < Event
6
6
  channel 'protege.inference.started'
7
7
 
8
- def persona = self[:persona]
8
+ def agent = self[:agent]
9
9
  def message = self[:message]
10
10
  end
11
11
  end
@@ -6,6 +6,6 @@ module Protege
6
6
  channel 'protege.loop.run.completed'
7
7
 
8
8
  def name = self[:name]
9
- def persona_name = self[:persona_name]
9
+ def agent_name = self[:agent_name]
10
10
  end
11
11
  end
@@ -7,6 +7,6 @@ module Protege
7
7
  channel 'protege.loop.run.enqueued'
8
8
 
9
9
  def name = self[:name]
10
- def persona_name = self[:persona_name]
10
+ def agent_name = self[:agent_name]
11
11
  end
12
12
  end
@@ -6,7 +6,7 @@ module Protege
6
6
  channel 'protege.loop.run.failed'
7
7
 
8
8
  def name = self[:name]
9
- def persona_name = self[:persona_name]
9
+ def agent_name = self[:agent_name]
10
10
  def error = self[:error]
11
11
  end
12
12
  end
@@ -6,6 +6,6 @@ module Protege
6
6
  channel 'protege.loop.run.started'
7
7
 
8
8
  def name = self[:name]
9
- def persona_name = self[:persona_name]
9
+ def agent_name = self[:agent_name]
10
10
  end
11
11
  end
@@ -5,7 +5,7 @@ module Protege
5
5
  class ToolCallCompletedEvent < Event
6
6
  channel 'protege.inference.tool_call.completed'
7
7
 
8
- def persona = self[:persona]
8
+ def agent = self[:agent]
9
9
  def tool_call = self[:tool_call]
10
10
  def result = self[:result]
11
11
  end
@@ -5,7 +5,7 @@ module Protege
5
5
  class ToolCallFailedEvent < Event
6
6
  channel 'protege.inference.tool_call.failed'
7
7
 
8
- def persona = self[:persona]
8
+ def agent = self[:agent]
9
9
  def tool_call = self[:tool_call]
10
10
  def error = self[:error]
11
11
  end
@@ -5,7 +5,7 @@ module Protege
5
5
  class ToolCallStartedEvent < Event
6
6
  channel 'protege.inference.tool_call.started'
7
7
 
8
- def persona = self[:persona]
8
+ def agent = self[:agent]
9
9
  def tool_call = self[:tool_call]
10
10
  end
11
11
  end
@@ -5,7 +5,7 @@ module Protege
5
5
  class ToolCallsReceivedEvent < Event
6
6
  channel 'protege.inference.tool_calls.received'
7
7
 
8
- def persona = self[:persona]
8
+ def agent = self[:agent]
9
9
  def tool_calls = self[:tool_calls]
10
10
  end
11
11
  end
@@ -9,5 +9,6 @@ module Protege
9
9
  # installed once at boot.
10
10
  class Hook
11
11
  include HookMixin
12
+ include Protege::Manifested
12
13
  end
13
14
  end
@@ -17,7 +17,7 @@ module Protege
17
17
  # @example
18
18
  # class EventLoggerHook < Protege::Hook
19
19
  # on Protege::InferenceStartedEvent do |event|
20
- # log "START #{event.persona&.name}"
20
+ # log "START #{event.agent&.name}"
21
21
  # end
22
22
  #
23
23
  # on Protege::ToolCallStartedEvent, Protege::ToolCallCompletedEvent do |event|
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protege
4
+ # The read-only facade over the extension registries — one place the dashboard asks "what extensions of
5
+ # this type exist, and what is their metadata?". Generalizes the tools-only +ToolMixin.registered+ into a
6
+ # uniform lookup across all four types, each row a plain {Entry} carrying the {Manifested} metadata plus
7
+ # the class itself.
8
+ module Manifest
9
+ # One registered extension, flattened to its manifest metadata for the UI and for bindings. +summary+
10
+ # is the short human-facing blurb; +description+ is the verbose, LLM-facing text (a tool's schema text).
11
+ Entry = Data.define(:type, :id, :display_name, :summary, :description, :klass)
12
+
13
+ # The extension base class for each manifest type (referenced lazily so the facade loads before them).
14
+ BASES = {
15
+ tool: 'Protege::Tool',
16
+ provider: 'Protege::Provider',
17
+ hook: 'Protege::Hook',
18
+ resolver: 'Protege::Resolver'
19
+ }.freeze
20
+
21
+ class << self
22
+ # Every registered extension of a type, as {Entry}s sorted by display name.
23
+ #
24
+ # @param type [Symbol] one of +:tool+, +:provider+, +:hook+, +:resolver+
25
+ # @return [Array<Entry>] the registered extensions' manifest entries
26
+ def registered(type)
27
+ base(type).registered.map { |klass| entry_for(type:, klass:) }.sort_by(&:display_name)
28
+ end
29
+
30
+ # The manifest entry for a single extension id, or nil when none is registered under it.
31
+ #
32
+ # @param type [Symbol] the manifest type
33
+ # @param id [Symbol] the extension id to look up
34
+ # @return [Entry, nil] the matching entry, or nil
35
+ def entry(type:, id:)
36
+ klass = base(type).registered.find { |candidate| candidate.id == id }
37
+ entry_for(type:, klass:) if klass
38
+ end
39
+
40
+ private
41
+
42
+ # Resolve a manifest type to its extension base class.
43
+ #
44
+ # @param type [Symbol] the manifest type
45
+ # @return [Class] the extension base class
46
+ def base(type)
47
+ BASES.fetch(type).constantize
48
+ end
49
+
50
+ # Build an {Entry} from a registered extension class.
51
+ #
52
+ # @param type [Symbol] the manifest type
53
+ # @param klass [Class] the extension class
54
+ # @return [Entry] the manifest entry
55
+ def entry_for(type:, klass:)
56
+ Entry.new(
57
+ type:,
58
+ id: klass.id,
59
+ display_name: klass.display_name,
60
+ summary: klass.summary,
61
+ description: klass.description,
62
+ klass:
63
+ )
64
+ end
65
+ end
66
+ end
67
+ end