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.
- checksums.yaml +4 -4
- data/MIT-LICENSE +21 -0
- data/Rakefile +1 -3
- data/app/assets/builds/protege.css +76 -83
- data/app/controllers/concerns/protege/agent_scoped.rb +22 -0
- data/app/controllers/concerns/protege/agent_toolkit_scoped.rb +22 -0
- data/app/controllers/protege/access_rules_controller.rb +13 -13
- data/app/controllers/protege/agent_toolkit_rules_controller.rb +62 -0
- data/app/controllers/protege/agent_toolkits_controller.rb +44 -0
- data/app/controllers/protege/agents/agent_toolkits_controller.rb +65 -0
- data/app/controllers/protege/agents_controller.rb +118 -0
- data/app/controllers/protege/archives_controller.rb +18 -18
- data/app/controllers/protege/home_controller.rb +1 -1
- data/app/controllers/protege/messages_controller.rb +1 -1
- data/app/controllers/protege/replies_controller.rb +3 -3
- data/app/controllers/protege/responsibilities_controller.rb +7 -7
- data/app/controllers/protege/threads_controller.rb +4 -4
- data/app/controllers/protege/toolkits_controller.rb +84 -0
- data/app/helpers/protege/agents_helper.rb +75 -0
- data/app/helpers/protege/application_helper.rb +3 -2
- data/app/helpers/protege/components/badges_helper.rb +11 -2
- data/app/helpers/protege/components/forms_helper.rb +47 -12
- data/app/helpers/protege/components/typography_helper.rb +1 -1
- data/app/helpers/protege/messages_helper.rb +2 -2
- data/app/helpers/protege/responsibilities_helper.rb +2 -2
- data/app/helpers/protege/threads_helper.rb +11 -11
- data/app/helpers/protege/toolkits_helper.rb +66 -0
- data/app/hooks/protege/event_logger_hook.rb +8 -8
- data/app/jobs/protege/console_inference_job.rb +8 -8
- data/app/jobs/protege/inference_job.rb +11 -11
- data/app/jobs/protege/responsibility_job.rb +10 -10
- data/app/mailboxes/protege/agent_mailbox.rb +54 -29
- data/app/mailers/protege/alert_mailer.rb +41 -0
- data/app/mailers/protege/application_mailer.rb +9 -8
- data/app/models/concerns/protege/broadcastable_agent.rb +50 -0
- data/app/models/concerns/protege/broadcastable_toolkit.rb +49 -0
- data/app/models/concerns/protege/toolable.rb +108 -0
- data/app/models/protege/access_rule.rb +12 -12
- data/app/models/protege/{persona.rb → agent.rb} +45 -45
- data/app/models/protege/agent_toolkit.rb +50 -0
- data/app/models/protege/agent_toolkit_rule.rb +25 -0
- data/app/models/protege/application_record.rb +1 -1
- data/app/models/protege/email_thread.rb +16 -13
- data/app/models/protege/message.rb +36 -20
- data/app/models/protege/responsibility.rb +12 -12
- data/app/models/protege/tool_use.rb +6 -6
- data/app/models/protege/toolkit.rb +115 -0
- data/app/models/protege/trace.rb +1 -1
- data/app/providers/protege/open_router_provider.rb +17 -0
- data/app/resolvers/README.md +4 -4
- data/app/resolvers/protege/load_file_resolver.rb +5 -5
- data/app/resolvers/protege/load_text_resolver.rb +3 -3
- data/app/resolvers/protege/thread_history_resolver.rb +1 -1
- data/app/services/protege/dev_setup.rb +110 -0
- data/app/services/protege/message_search.rb +2 -2
- data/app/services/protege/system_toolkits.rb +80 -0
- data/app/tools/protege/create_file_tool.rb +2 -0
- data/app/tools/protege/read_attachment_tool.rb +3 -1
- data/app/tools/protege/search_emails_tool.rb +15 -13
- data/app/tools/protege/send_email_tool.rb +60 -18
- data/app/tools/protege/web_fetch_tool.rb +2 -0
- data/app/tools/protege/web_search_tool.rb +3 -1
- data/app/views/layouts/mailer.text.erb +1 -0
- data/app/views/protege/access_rules/create.turbo_stream.slim +3 -3
- data/app/views/protege/agent_toolkit_rules/create.turbo_stream.slim +6 -0
- data/app/views/protege/agent_toolkits/_gate_rule.html.slim +7 -0
- data/app/views/protege/agent_toolkits/_gate_rule_form.html.slim +8 -0
- data/app/views/protege/agent_toolkits/_gate_rules.html.slim +19 -0
- data/app/views/protege/agent_toolkits/show.html.slim +17 -0
- data/app/views/protege/{personas → agents}/_access_rule.html.slim +1 -1
- data/app/views/protege/{personas → agents}/_access_rule_form.html.slim +1 -1
- data/app/views/protege/{personas → agents}/_access_rules.html.slim +5 -5
- data/app/views/protege/agents/_actions.html.slim +15 -0
- data/app/views/protege/agents/_agent_sidebar_item.html.slim +5 -0
- data/app/views/protege/agents/_agent_toolkit.html.slim +7 -0
- data/app/views/protege/agents/_agent_toolkit_form.html.slim +7 -0
- data/app/views/protege/agents/_form.html.slim +25 -0
- data/app/views/protege/agents/_sidebar.html.slim +11 -0
- data/app/views/protege/agents/_toolkits.html.slim +19 -0
- data/app/views/protege/agents/agent_toolkits/create.turbo_stream.slim +6 -0
- data/app/views/protege/agents/edit.html.slim +7 -0
- data/app/views/protege/agents/index.html.slim +6 -0
- data/app/views/protege/agents/new.html.slim +7 -0
- data/app/views/protege/agents/show.html.slim +29 -0
- data/app/views/protege/alert_mailer/inference_failed.text.erb +4 -0
- data/app/views/protege/email_domains/show.html.slim +1 -1
- data/app/views/protege/home/show.html.slim +13 -13
- data/app/views/protege/responsibilities/_form.html.slim +2 -2
- data/app/views/protege/responsibilities/_responsibility_sidebar_item.html.slim +1 -1
- data/app/views/protege/responsibilities/new.html.slim +1 -1
- data/app/views/protege/responsibilities/show.html.slim +2 -2
- data/app/views/protege/responsibility_runs/show.html.slim +1 -1
- data/app/views/protege/shared/_header.html.slim +2 -1
- data/app/views/protege/shared/_hotkeys.html.slim +15 -11
- data/app/views/protege/threads/new.html.slim +2 -2
- data/app/views/protege/toolkits/_form.html.slim +13 -0
- data/app/views/protege/toolkits/_sidebar.html.slim +11 -0
- data/app/views/protege/toolkits/_toolkit_sidebar_item.html.slim +5 -0
- data/app/views/protege/toolkits/edit.html.slim +7 -0
- data/app/views/protege/toolkits/index.html.slim +6 -0
- data/app/views/protege/toolkits/new.html.slim +7 -0
- data/app/views/protege/toolkits/show.html.slim +13 -0
- data/config/routes.rb +12 -1
- data/db/migrate/20260707120000_replace_persona_active_with_archived_at.rb +1 -1
- data/db/migrate/20260708120000_add_disabled_tool_ids_to_personas.rb +1 -1
- data/db/migrate/20260728140000_scope_message_and_thread_uniqueness_per_persona.rb +16 -0
- data/db/migrate/20260812000001_create_protege_toolkits.rb +37 -0
- data/db/migrate/20260815000001_remove_disabled_tool_ids_from_personas.rb +21 -0
- data/db/migrate/20260815000002_add_key_to_protege_toolkits.rb +22 -0
- data/db/migrate/20260815100000_rename_personas_to_agents.rb +24 -0
- data/lib/generators/protege/agent/agent_generator.rb +35 -0
- data/lib/generators/protege/{persona/templates/persona.rb.tt → agent/templates/agent.rb.tt} +9 -9
- data/lib/generators/protege/extension_naming.rb +1 -1
- data/lib/generators/protege/hook/templates/hook.rb.tt +2 -2
- data/lib/generators/protege/install/install_generator.rb +19 -11
- data/lib/generators/protege/install/templates/initializer.rb.tt +17 -6
- data/lib/generators/protege/postfix/templates/deploy/mail/MAIL.md +2 -2
- data/lib/generators/protege/resolver/resolver_generator.rb +2 -2
- data/lib/generators/protege/resolver/templates/resolver.rb.tt +4 -4
- data/lib/generators/protege/tool/templates/tool.rb.tt +7 -5
- data/lib/protege/configuration.rb +43 -8
- data/lib/protege/engine.rb +3 -1
- data/lib/protege/errors/tool_not_available_error.rb +7 -6
- data/lib/protege/events/event.rb +2 -2
- data/lib/protege/events/inference_chunk_event.rb +1 -1
- data/lib/protege/events/inference_completed_event.rb +1 -1
- data/lib/protege/events/inference_failed_event.rb +1 -1
- data/lib/protege/events/inference_generated_event.rb +1 -1
- data/lib/protege/events/inference_max_turns_reached_event.rb +1 -1
- data/lib/protege/events/inference_started_event.rb +1 -1
- data/lib/protege/events/loop_run_completed_event.rb +1 -1
- data/lib/protege/events/loop_run_enqueued_event.rb +1 -1
- data/lib/protege/events/loop_run_failed_event.rb +1 -1
- data/lib/protege/events/loop_run_started_event.rb +1 -1
- data/lib/protege/events/tool_call_completed_event.rb +1 -1
- data/lib/protege/events/tool_call_failed_event.rb +1 -1
- data/lib/protege/events/tool_call_started_event.rb +1 -1
- data/lib/protege/events/tool_calls_received_event.rb +1 -1
- data/lib/protege/extensions/hook.rb +1 -0
- data/lib/protege/extensions/hook_mixin.rb +1 -1
- data/lib/protege/extensions/manifest.rb +67 -0
- data/lib/protege/extensions/manifested.rb +81 -0
- data/lib/protege/extensions/provider.rb +1 -0
- data/lib/protege/extensions/provider_mixin.rb +9 -17
- data/lib/protege/extensions/resolver.rb +2 -1
- data/lib/protege/extensions/resolver_mixin.rb +2 -2
- data/lib/protege/extensions/tool.rb +1 -0
- data/lib/protege/extensions/tool_mixin.rb +16 -40
- data/lib/protege/gateway/access_control.rb +19 -19
- data/lib/protege/gateway/access_policy.rb +2 -2
- data/lib/protege/gateway/mail/outbound.rb +22 -5
- data/lib/protege/gateway.rb +48 -37
- data/lib/protege/loop/scheduler.rb +1 -1
- data/lib/protege/orchestrator/context.rb +8 -8
- data/lib/protege/orchestrator/harness.rb +26 -24
- data/lib/protege/orchestrator/reply_context.rb +3 -3
- data/lib/protege/orchestrator/reply_harness.rb +8 -8
- data/lib/protege/orchestrator/resolver_chain.rb +3 -3
- data/lib/protege/orchestrator/responsibility_context.rb +4 -4
- data/lib/protege/orchestrator/responsibility_harness.rb +8 -8
- data/lib/protege/orchestrator.rb +7 -7
- data/lib/protege/subscribers/alerter.rb +117 -0
- data/lib/protege/subscribers/tracing.rb +1 -1
- data/lib/protege/version.rb +1 -1
- data/lib/protege.rb +2 -2
- data/lib/tasks/protege_tasks.rake +40 -0
- metadata +67 -23
- data/app/controllers/concerns/protege/persona_scoped.rb +0 -22
- data/app/controllers/protege/personas_controller.rb +0 -118
- data/app/helpers/protege/personas_helper.rb +0 -90
- data/app/models/concerns/protege/broadcastable_persona.rb +0 -50
- data/app/models/concerns/protege/tool_scoped.rb +0 -88
- data/app/views/protege/personas/_actions.html.slim +0 -15
- data/app/views/protege/personas/_form.html.slim +0 -29
- data/app/views/protege/personas/_persona_sidebar_item.html.slim +0 -5
- data/app/views/protege/personas/_sidebar.html.slim +0 -11
- data/app/views/protege/personas/edit.html.slim +0 -7
- data/app/views/protege/personas/index.html.slim +0 -6
- data/app/views/protege/personas/new.html.slim +0 -7
- data/app/views/protege/personas/show.html.slim +0 -37
- 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 '../
|
|
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
|
|
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
|
|
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 +
|
|
80
|
-
# truth for the
|
|
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
|
|
84
|
-
invoke Protege::Generators::
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
20
|
-
# config.max_tool_turns =
|
|
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
|
|
51
|
-
# Per-
|
|
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,
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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.
|
|
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.
|
|
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
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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 +
|
|
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
|
|
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
|
-
# +:
|
|
54
|
-
# +app/tools+, +app/resolvers+, +app/hooks+, +app/
|
|
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, :
|
|
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 =
|
|
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
|
-
@
|
|
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*
|
|
143
|
-
# per-
|
|
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
|
|
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
|
data/lib/protege/engine.rb
CHANGED
|
@@ -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
|
|
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
|
|
5
|
-
#
|
|
6
|
-
# does not exist at all): here the tool exists but is withheld from
|
|
7
|
-
# it before +#use+ runs and the model gets a serialized failure to
|
|
8
|
-
# where a tool
|
|
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
|
|
11
|
+
def initialize(name:) = super("tool :#{name} is not available to this agent")
|
|
11
12
|
end
|
|
12
13
|
end
|
data/lib/protege/events/event.rb
CHANGED
|
@@ -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(
|
|
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.
|
|
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
|
# 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
|
|
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'
|
|
@@ -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.
|
|
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
|