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.
- 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/message_search.rb +2 -2
- data/app/services/protege/system_toolkits.rb +67 -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 +16 -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 +12 -0
- metadata +66 -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
|
@@ -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
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Uniform, UI-facing metadata for every registered extension — the "Manifest" layer of the extension
|
|
5
|
+
# framework. Mixed once into each extension base class (+Protege::Tool+/+Provider+/+Hook+/+Resolver+) so
|
|
6
|
+
# a dashboard can list any of them by the same handful of attributes, regardless of how each type
|
|
7
|
+
# registers or activates. Defining the surface here, in one concern, is what lets bindings (e.g.
|
|
8
|
+
# Toolkits) reference extensions uniformly instead of hand-wiring a registry per type.
|
|
9
|
+
#
|
|
10
|
+
# Provides, at the class level: a stable +id+ (name-derived, or explicit via {ClassMethods#manifest_id}),
|
|
11
|
+
# a human +display_name+ (defaulting to the titleized id), an optional +description+, and +registered+
|
|
12
|
+
# (the loaded, named concrete subclasses of the base it is called on). Enumeration is descendant-based,
|
|
13
|
+
# like the tools registry, so it survives Zeitwerk reloads and skips anonymous classes.
|
|
14
|
+
module Manifested
|
|
15
|
+
extend ActiveSupport::Concern
|
|
16
|
+
|
|
17
|
+
# Class-level metadata surface added to every extension base.
|
|
18
|
+
module ClassMethods
|
|
19
|
+
# The extension's stable symbolic id — the manifest key a binding references and the dashboard
|
|
20
|
+
# shows. Defaults to the class basename with a trailing +Tool+/+Provider+/+Hook+/+Resolver+ suffix
|
|
21
|
+
# stripped and the rest snake_cased; override with {#manifest_id} when the id must be explicit (a
|
|
22
|
+
# provider, whose id is matched against +config.provider_id+). Memoized per class.
|
|
23
|
+
#
|
|
24
|
+
# @return [Symbol] the extension id
|
|
25
|
+
def id
|
|
26
|
+
@id ||= name.demodulize.sub(/(?:Tool|Provider|Hook|Resolver)\z/, '').underscore.to_sym
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Declare an explicit id, overriding the name-derived default.
|
|
30
|
+
#
|
|
31
|
+
# @param value [Symbol, String] the explicit id
|
|
32
|
+
# @return [Symbol] the stored id
|
|
33
|
+
def manifest_id(value)
|
|
34
|
+
@id = value.to_sym
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# A human-readable label for the dashboard: the declared name, or the titleized id by default. Pass
|
|
38
|
+
# a value to set it.
|
|
39
|
+
#
|
|
40
|
+
# @param value [String, nil] the display name to set, or nil to read
|
|
41
|
+
# @return [String] the display name
|
|
42
|
+
def display_name(value = nil)
|
|
43
|
+
return @display_name || id.to_s.titleize if value.nil?
|
|
44
|
+
|
|
45
|
+
@display_name = value
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# A natural-language description of the extension: the declared value, or nil when unset. Pass a
|
|
49
|
+
# value to set it. For a tool this is the *LLM-facing* text published in the model's tool schema —
|
|
50
|
+
# verbose and model-directed. For a short, human-facing dashboard blurb, use {#summary} instead.
|
|
51
|
+
#
|
|
52
|
+
# @param value [String, nil] the description to set, or nil to read
|
|
53
|
+
# @return [String, nil] the description
|
|
54
|
+
def description(value = nil)
|
|
55
|
+
return @description if value.nil?
|
|
56
|
+
|
|
57
|
+
@description = value
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# A short, human-facing one-liner for the dashboard — what an operator reads when picking this
|
|
61
|
+
# extension. Distinct from {#description}, which for tools is the verbose text sent to the model.
|
|
62
|
+
# The declared value, or nil when unset. Pass a value to set it.
|
|
63
|
+
#
|
|
64
|
+
# @param value [String, nil] the summary to set, or nil to read
|
|
65
|
+
# @return [String, nil] the summary
|
|
66
|
+
def summary(value = nil)
|
|
67
|
+
return @summary if value.nil?
|
|
68
|
+
|
|
69
|
+
@summary = value
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The loaded, named concrete subclasses of this extension base — its registry. Anonymous classes
|
|
73
|
+
# (nil name) are skipped; descendant-based so it survives Zeitwerk reloads without bookkeeping.
|
|
74
|
+
#
|
|
75
|
+
# @return [Array<Class>] the registered extension classes
|
|
76
|
+
def registered
|
|
77
|
+
descendants.reject { _1.name.nil? }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -80,7 +80,7 @@ module Protege
|
|
|
80
80
|
|
|
81
81
|
# The model id this provider targets, read from its own +config.providers+ slice (+:model+).
|
|
82
82
|
# Public so the harness can record which model produced a turn (for tracing); resolved per call, so
|
|
83
|
-
# a future
|
|
83
|
+
# a future agent-level override is reflected here. Providers may treat a missing model as a hard
|
|
84
84
|
# error at generation time — that enforcement is theirs, not this soft reader's.
|
|
85
85
|
#
|
|
86
86
|
# @return [String, nil] the configured model id, or nil when unset
|
|
@@ -97,27 +97,19 @@ module Protege
|
|
|
97
97
|
{}
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
-
# Class-level DSL provided by the mixin.
|
|
100
|
+
# Class-level DSL provided by the mixin. The provider's +id+ (and the rest of its manifest metadata)
|
|
101
|
+
# comes from the shared +Extensions::Manifested+ concern; +protege_id+ is the provider-facing name for
|
|
102
|
+
# its explicit-id override, kept because a provider's id must match +config.provider_id+ exactly rather
|
|
103
|
+
# than be derived from the class name.
|
|
101
104
|
module ClassMethods
|
|
102
|
-
# Declare the symbolic identifier this provider responds to
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
+
# Declare the symbolic identifier this provider responds to — an explicit override of the
|
|
106
|
+
# name-derived manifest id. Call inside the class body; the configured +provider_id+ references the
|
|
107
|
+
# provider by this symbol.
|
|
105
108
|
#
|
|
106
109
|
# @param symbol [Symbol] the provider's unique identifier (e.g. +:openrouter+).
|
|
107
110
|
# @return [Symbol] the stored identifier.
|
|
108
111
|
def protege_id(symbol)
|
|
109
|
-
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Return the declared symbolic id.
|
|
113
|
-
#
|
|
114
|
-
# @return [Symbol] the id set via +protege_id+.
|
|
115
|
-
# @raise [Protege::ContractViolationError] when the class never called +protege_id+.
|
|
116
|
-
def id
|
|
117
|
-
@protege_id || raise(
|
|
118
|
-
Protege::ContractViolationError,
|
|
119
|
-
"#{self} must declare its id with `protege_id :<symbol>` inside the class body"
|
|
120
|
-
)
|
|
112
|
+
manifest_id(symbol)
|
|
121
113
|
end
|
|
122
114
|
end
|
|
123
115
|
end
|
|
@@ -5,8 +5,9 @@ module Protege
|
|
|
5
5
|
# prompts, history, retrieved knowledge). Host apps subclass +Protege::Resolver+ and implement
|
|
6
6
|
# +#resolve(context:)+, returning an +Array<Protege::ModelMessage>+ or nil. The contract lives
|
|
7
7
|
# in +ResolverMixin+, which this includes. Unlike tools, resolvers are not auto-registered — they are
|
|
8
|
-
# added explicitly to
|
|
8
|
+
# added explicitly to an agent's +ResolverChain+.
|
|
9
9
|
class Resolver
|
|
10
10
|
include ResolverMixin
|
|
11
|
+
include Protege::Manifested
|
|
11
12
|
end
|
|
12
13
|
end
|
|
@@ -6,7 +6,7 @@ module Protege
|
|
|
6
6
|
# or +nil+ to contribute nothing. Resolvers are the Orchestrator's mechanism for assembling
|
|
7
7
|
# inference context — system prompts, conversation history, retrieved knowledge.
|
|
8
8
|
#
|
|
9
|
-
# Unlike +Tool+, resolvers are NOT auto-registered. They are explicitly added to
|
|
9
|
+
# Unlike +Tool+, resolvers are NOT auto-registered. They are explicitly added to an agent's
|
|
10
10
|
# +ResolverChain+ at declaration time. Host resolvers subclass +Protege::Resolver+, which
|
|
11
11
|
# includes this module.
|
|
12
12
|
#
|
|
@@ -20,7 +20,7 @@ module Protege
|
|
|
20
20
|
# @example
|
|
21
21
|
# class MyResolver < Protege::Resolver
|
|
22
22
|
# def resolve(context:)
|
|
23
|
-
# message(role: :system, content: "You are #{context.
|
|
23
|
+
# message(role: :system, content: "You are #{context.agent.name}.")
|
|
24
24
|
# end
|
|
25
25
|
# end
|
|
26
26
|
module ResolverMixin
|