protege 0.1.0.alpha.1
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 +7 -0
- data/README.md +24 -0
- data/Rakefile +12 -0
- data/app/assets/builds/protege.css +1128 -0
- data/app/assets/javascripts/protege/attachment_picker.js +163 -0
- data/app/assets/javascripts/protege/copy.js +80 -0
- data/app/assets/javascripts/protege/forms.js +28 -0
- data/app/assets/javascripts/protege/keys.js +212 -0
- data/app/assets/javascripts/protege/resize.js +149 -0
- data/app/assets/javascripts/protege/theme.js +25 -0
- data/app/assets/stylesheets/protege/application.css +15 -0
- data/app/assets/tailwind/application.css +137 -0
- data/app/controllers/concerns/protege/persona_scoped.rb +22 -0
- data/app/controllers/concerns/protege/thread_scoped.rb +22 -0
- data/app/controllers/protege/access_rules_controller.rb +72 -0
- data/app/controllers/protege/application_controller.rb +18 -0
- data/app/controllers/protege/archives_controller.rb +52 -0
- data/app/controllers/protege/email_domains_controller.rb +72 -0
- data/app/controllers/protege/home_controller.rb +15 -0
- data/app/controllers/protege/messages_controller.rb +46 -0
- data/app/controllers/protege/mta_domains_syncs_controller.rb +22 -0
- data/app/controllers/protege/personas_controller.rb +118 -0
- data/app/controllers/protege/replies_controller.rb +64 -0
- data/app/controllers/protege/responsibilities_controller.rb +82 -0
- data/app/controllers/protege/responsibility_runs_controller.rb +46 -0
- data/app/controllers/protege/threads_controller.rb +75 -0
- data/app/controllers/protege/traces/searches_controller.rb +19 -0
- data/app/controllers/protege/traces_controller.rb +52 -0
- data/app/helpers/protege/application_helper.rb +30 -0
- data/app/helpers/protege/components/badges_helper.rb +59 -0
- data/app/helpers/protege/components/buttons_helper.rb +141 -0
- data/app/helpers/protege/components/cards_helper.rb +48 -0
- data/app/helpers/protege/components/components_helper.rb +30 -0
- data/app/helpers/protege/components/dialogs_helper.rb +36 -0
- data/app/helpers/protege/components/forms_helper.rb +223 -0
- data/app/helpers/protege/components/hotkeys_helper.rb +60 -0
- data/app/helpers/protege/components/icons_helper.rb +41 -0
- data/app/helpers/protege/components/introspection_helper.rb +162 -0
- data/app/helpers/protege/components/layout_helper.rb +169 -0
- data/app/helpers/protege/components/menus_helper.rb +68 -0
- data/app/helpers/protege/components/pagination_helper.rb +100 -0
- data/app/helpers/protege/components/sidebar_helper.rb +211 -0
- data/app/helpers/protege/components/tables_helper.rb +59 -0
- data/app/helpers/protege/components/typography_helper.rb +64 -0
- data/app/helpers/protege/email_domains_helper.rb +22 -0
- data/app/helpers/protege/messages_helper.rb +152 -0
- data/app/helpers/protege/personas_helper.rb +90 -0
- data/app/helpers/protege/responsibilities_helper.rb +63 -0
- data/app/helpers/protege/threads_helper.rb +118 -0
- data/app/helpers/protege/trace_helper.rb +22 -0
- data/app/hooks/protege/event_logger_hook.rb +109 -0
- data/app/jobs/protege/application_job.rb +41 -0
- data/app/jobs/protege/console_inference_job.rb +57 -0
- data/app/jobs/protege/inference_job.rb +60 -0
- data/app/jobs/protege/mail_provisioning_job.rb +21 -0
- data/app/jobs/protege/responsibility_job.rb +70 -0
- data/app/jobs/protege/responsibility_tick_job.rb +19 -0
- data/app/mailboxes/protege/agent_mailbox.rb +123 -0
- data/app/mailers/protege/application_mailer.rb +65 -0
- data/app/models/concerns/protege/broadcastable_email_domain.rb +48 -0
- data/app/models/concerns/protege/broadcastable_message.rb +47 -0
- data/app/models/concerns/protege/broadcastable_persona.rb +50 -0
- data/app/models/concerns/protege/broadcastable_responsibility.rb +51 -0
- data/app/models/concerns/protege/broadcastable_thread.rb +55 -0
- data/app/models/concerns/protege/tool_scoped.rb +88 -0
- data/app/models/protege/access_rule.rb +50 -0
- data/app/models/protege/application_record.rb +18 -0
- data/app/models/protege/current.rb +23 -0
- data/app/models/protege/email_domain.rb +136 -0
- data/app/models/protege/email_thread.rb +81 -0
- data/app/models/protege/message.rb +401 -0
- data/app/models/protege/persona.rb +229 -0
- data/app/models/protege/responsibility.rb +98 -0
- data/app/models/protege/responsibility_run.rb +68 -0
- data/app/models/protege/tool_use.rb +115 -0
- data/app/models/protege/trace.rb +71 -0
- data/app/providers/protege/open_router_provider.rb +317 -0
- data/app/resolvers/README.md +255 -0
- data/app/resolvers/protege/load_file_resolver.rb +86 -0
- data/app/resolvers/protege/load_record_resolver.rb +80 -0
- data/app/resolvers/protege/load_text_resolver.rb +40 -0
- data/app/resolvers/protege/thread_history_resolver.rb +116 -0
- data/app/services/protege/introspection_tool_call_presentation.rb +134 -0
- data/app/services/protege/message_search.rb +114 -0
- data/app/services/protege/trace_search.rb +102 -0
- data/app/subscribers/protege/introspection_broadcaster.rb +324 -0
- data/app/tools/protege/create_file_tool.rb +109 -0
- data/app/tools/protege/read_attachment_tool.rb +101 -0
- data/app/tools/protege/search_emails_tool.rb +129 -0
- data/app/tools/protege/send_email_tool.rb +240 -0
- data/app/tools/protege/web_fetch_tool.rb +224 -0
- data/app/tools/protege/web_search_tool.rb +131 -0
- data/app/views/layouts/protege/application.html.slim +19 -0
- data/app/views/protege/access_rules/create.turbo_stream.slim +6 -0
- data/app/views/protege/email_domains/_email_domain_sidebar_item.html.slim +4 -0
- data/app/views/protege/email_domains/_form.html.slim +10 -0
- data/app/views/protege/email_domains/_sidebar.html.slim +11 -0
- data/app/views/protege/email_domains/index.html.slim +6 -0
- data/app/views/protege/email_domains/new.html.slim +7 -0
- data/app/views/protege/email_domains/show.html.slim +36 -0
- data/app/views/protege/home/show.html.slim +26 -0
- data/app/views/protege/messages/_message.html.slim +19 -0
- data/app/views/protege/messages/_sidebar.html.slim +13 -0
- data/app/views/protege/messages/index.html.slim +20 -0
- data/app/views/protege/personas/_access_rule.html.slim +7 -0
- data/app/views/protege/personas/_access_rule_form.html.slim +8 -0
- data/app/views/protege/personas/_access_rules.html.slim +19 -0
- data/app/views/protege/personas/_actions.html.slim +15 -0
- data/app/views/protege/personas/_form.html.slim +29 -0
- data/app/views/protege/personas/_persona_sidebar_item.html.slim +5 -0
- data/app/views/protege/personas/_sidebar.html.slim +11 -0
- data/app/views/protege/personas/edit.html.slim +7 -0
- data/app/views/protege/personas/index.html.slim +6 -0
- data/app/views/protege/personas/new.html.slim +7 -0
- data/app/views/protege/personas/show.html.slim +37 -0
- data/app/views/protege/responsibilities/_form.html.slim +21 -0
- data/app/views/protege/responsibilities/_responsibility_sidebar_item.html.slim +5 -0
- data/app/views/protege/responsibilities/_sidebar.html.slim +11 -0
- data/app/views/protege/responsibilities/edit.html.slim +7 -0
- data/app/views/protege/responsibilities/index.html.slim +6 -0
- data/app/views/protege/responsibilities/new.html.slim +7 -0
- data/app/views/protege/responsibilities/show.html.slim +22 -0
- data/app/views/protege/responsibility_runs/show.html.slim +13 -0
- data/app/views/protege/shared/_confirm_dialog.html.slim +22 -0
- data/app/views/protege/shared/_header.html.slim +14 -0
- data/app/views/protege/shared/_hotkeys.html.slim +43 -0
- data/app/views/protege/threads/_composer_footer.html.slim +15 -0
- data/app/views/protege/threads/_introspection.html.slim +7 -0
- data/app/views/protege/threads/_reasoning.html.slim +4 -0
- data/app/views/protege/threads/_reply_form.html.slim +7 -0
- data/app/views/protege/threads/_run_marker.html.slim +3 -0
- data/app/views/protege/threads/_sidebar.html.slim +15 -0
- data/app/views/protege/threads/_thread.html.slim +6 -0
- data/app/views/protege/threads/_tool_call.html.slim +8 -0
- data/app/views/protege/threads/index.html.slim +6 -0
- data/app/views/protege/threads/new.html.slim +15 -0
- data/app/views/protege/threads/show.html.slim +25 -0
- data/app/views/protege/traces/_sidebar.html.slim +15 -0
- data/app/views/protege/traces/index.html.slim +11 -0
- data/app/views/protege/traces/searches/_sidebar.html.slim +13 -0
- data/app/views/protege/traces/searches/show.html.slim +22 -0
- data/app/views/protege/traces/show.html.slim +28 -0
- data/config/routes.rb +33 -0
- data/db/migrate/20260520000001_create_protege_tables.rb +105 -0
- data/db/migrate/20260706120130_create_protege_traces.rb +30 -0
- data/db/migrate/20260707120000_replace_persona_active_with_archived_at.rb +24 -0
- data/db/migrate/20260708120000_add_disabled_tool_ids_to_personas.rb +22 -0
- data/lib/generators/protege/extension_naming.rb +38 -0
- data/lib/generators/protege/hook/hook_generator.rb +33 -0
- data/lib/generators/protege/hook/templates/hook.rb.tt +36 -0
- data/lib/generators/protege/install/install_generator.rb +170 -0
- data/lib/generators/protege/install/templates/initializer.rb.tt +84 -0
- data/lib/generators/protege/persona/persona_generator.rb +35 -0
- data/lib/generators/protege/persona/templates/persona.rb.tt +31 -0
- data/lib/generators/protege/postfix/postfix_generator.rb +74 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/Dockerfile +43 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/KeyTable +3 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/MAIL.md +135 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/README.md +56 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/SigningTable +3 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/TrustedHosts +8 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/entrypoint.sh +118 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/kamal-accessory.example.yml +32 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/main.cf +68 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/opendkim.conf +36 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/opendmarc.conf +32 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/opendmarc.ignore.hosts +9 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/policyd-spf.conf +21 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/provision.py +182 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/relay-to-app.sh +28 -0
- data/lib/generators/protege/postfix/templates/github/workflows/build-mail-image.example.yml +50 -0
- data/lib/generators/protege/provider/provider_generator.rb +44 -0
- data/lib/generators/protege/provider/templates/provider.rb.tt +50 -0
- data/lib/generators/protege/resolver/resolver_generator.rb +34 -0
- data/lib/generators/protege/resolver/templates/resolver.rb.tt +31 -0
- data/lib/generators/protege/tool/templates/tool.rb.tt +50 -0
- data/lib/generators/protege/tool/tool_generator.rb +34 -0
- data/lib/protege/configuration.rb +171 -0
- data/lib/protege/engine.rb +103 -0
- data/lib/protege/errors/bad_request_error.rb +8 -0
- data/lib/protege/errors/contract_violation_error.rb +8 -0
- data/lib/protege/errors/error.rb +30 -0
- data/lib/protege/errors/generic_error.rb +7 -0
- data/lib/protege/errors/internal_error.rb +8 -0
- data/lib/protege/errors/invalid_model_error.rb +8 -0
- data/lib/protege/errors/invalid_tool_result_error.rb +9 -0
- data/lib/protege/errors/rate_limited_error.rb +8 -0
- data/lib/protege/errors/resolver_file_not_found_error.rb +9 -0
- data/lib/protege/errors/response_parse_failed_error.rb +8 -0
- data/lib/protege/errors/timeout_error.rb +8 -0
- data/lib/protege/errors/tool_not_available_error.rb +12 -0
- data/lib/protege/errors/tool_not_found_error.rb +9 -0
- data/lib/protege/errors/unauthorized_error.rb +8 -0
- data/lib/protege/errors/unavailable_error.rb +8 -0
- data/lib/protege/errors/unsupported_capability_error.rb +8 -0
- data/lib/protege/events/event.rb +98 -0
- data/lib/protege/events/inference_chunk_event.rb +17 -0
- data/lib/protege/events/inference_completed_event.rb +12 -0
- data/lib/protege/events/inference_failed_event.rb +12 -0
- data/lib/protege/events/inference_generated_event.rb +19 -0
- data/lib/protege/events/inference_max_turns_reached_event.rb +12 -0
- data/lib/protege/events/inference_started_event.rb +11 -0
- data/lib/protege/events/loop_run_completed_event.rb +11 -0
- data/lib/protege/events/loop_run_enqueued_event.rb +12 -0
- data/lib/protege/events/loop_run_failed_event.rb +12 -0
- data/lib/protege/events/loop_run_started_event.rb +11 -0
- data/lib/protege/events/tool_call_completed_event.rb +12 -0
- data/lib/protege/events/tool_call_failed_event.rb +12 -0
- data/lib/protege/events/tool_call_started_event.rb +11 -0
- data/lib/protege/events/tool_calls_received_event.rb +11 -0
- data/lib/protege/extensions/hook.rb +13 -0
- data/lib/protege/extensions/hook_mixin.rb +90 -0
- data/lib/protege/extensions/provider.rb +12 -0
- data/lib/protege/extensions/provider_mixin.rb +124 -0
- data/lib/protege/extensions/resolver.rb +12 -0
- data/lib/protege/extensions/resolver_mixin.rb +58 -0
- data/lib/protege/extensions/tool.rb +12 -0
- data/lib/protege/extensions/tool_mixin.rb +217 -0
- data/lib/protege/gateway/access_control.rb +66 -0
- data/lib/protege/gateway/access_policy.rb +94 -0
- data/lib/protege/gateway/attachment_policy.rb +115 -0
- data/lib/protege/gateway/mail/address.rb +145 -0
- data/lib/protege/gateway/mail/attachment.rb +17 -0
- data/lib/protege/gateway/mail/message_id.rb +104 -0
- data/lib/protege/gateway/mail/outbound.rb +181 -0
- data/lib/protege/gateway/mail/references.rb +107 -0
- data/lib/protege/gateway.rb +398 -0
- data/lib/protege/inference/provider/request.rb +110 -0
- data/lib/protege/inference/provider/response.rb +50 -0
- data/lib/protege/inference/provider/tool_call.rb +52 -0
- data/lib/protege/inference.rb +121 -0
- data/lib/protege/loop/schedule.rb +66 -0
- data/lib/protege/loop/scheduler.rb +42 -0
- data/lib/protege/loop.rb +37 -0
- data/lib/protege/orchestrator/context.rb +65 -0
- data/lib/protege/orchestrator/harness.rb +305 -0
- data/lib/protege/orchestrator/reply_context.rb +21 -0
- data/lib/protege/orchestrator/reply_harness.rb +68 -0
- data/lib/protege/orchestrator/resolver_chain.rb +93 -0
- data/lib/protege/orchestrator/responsibility_context.rb +22 -0
- data/lib/protege/orchestrator/responsibility_harness.rb +60 -0
- data/lib/protege/orchestrator.rb +37 -0
- data/lib/protege/subscribers/hook_dispatcher.rb +140 -0
- data/lib/protege/subscribers/tracing.rb +65 -0
- data/lib/protege/types/document_part.rb +13 -0
- data/lib/protege/types/image_part.rb +11 -0
- data/lib/protege/types/model_message.rb +40 -0
- data/lib/protege/types/result.rb +113 -0
- data/lib/protege/types/stored_file.rb +127 -0
- data/lib/protege/types/text_part.rb +8 -0
- data/lib/protege/types/tool_result.rb +16 -0
- data/lib/protege/version.rb +6 -0
- data/lib/protege.rb +47 -0
- data/lib/tasks/protege_tasks.rake +16 -0
- metadata +494 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# See +Protege+ in protege.rb for the root namespace overview. This file reopens the module to
|
|
4
|
+
# define the process-wide +Configuration+ object and its +Protege.configure+ accessors.
|
|
5
|
+
module Protege
|
|
6
|
+
# Holds the process-wide Protege configuration set via +Protege.configure+. A single instance is
|
|
7
|
+
# memoized on the +Protege+ module and read throughout the LOGI pipeline — the Orchestrator reads
|
|
8
|
+
# +max_tool_turns+, the Inference layer reads +provider_id+ (each provider reads its own model /
|
|
9
|
+
# sampling / credentials from its +providers+ slice), and the Gateway reads +console_address+.
|
|
10
|
+
# Mutated once at boot from a Rails initializer; treated as read-only thereafter (the Orchestrator
|
|
11
|
+
# even freezes it per delivery via +Context+).
|
|
12
|
+
#
|
|
13
|
+
# @example Configure in a Rails initializer
|
|
14
|
+
# Protege.configure do |config|
|
|
15
|
+
# config.provider_id = :openrouter
|
|
16
|
+
# config.providers = { openrouter: { model: 'anthropic/claude-sonnet-4-5' } }
|
|
17
|
+
# end
|
|
18
|
+
class Configuration
|
|
19
|
+
# Generic, real-looking browser User-Agent sent by the built-in web tools (+web_search+,
|
|
20
|
+
# +web_fetch+) by default. Deliberately unbranded — a plain Chrome string that blends in — since
|
|
21
|
+
# some origins reject or fingerprint unusual agents.
|
|
22
|
+
DEFAULT_HTTP_USER_AGENT =
|
|
23
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' \
|
|
24
|
+
'(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
|
|
25
|
+
|
|
26
|
+
# Symbolic id of the inference provider extension (e.g. +:openrouter+). The model, sampling, and
|
|
27
|
+
# credentials for that provider live in its {#providers} slice, not here.
|
|
28
|
+
attr_accessor :provider_id
|
|
29
|
+
|
|
30
|
+
# Maximum tool-calling rounds before returning the last response.
|
|
31
|
+
attr_accessor :max_tool_turns
|
|
32
|
+
|
|
33
|
+
# Email address representing the dashboard user in local conversations. Messages from the
|
|
34
|
+
# dashboard use this as their +from_address+, and outbound replies to this address skip SMTP
|
|
35
|
+
# delivery.
|
|
36
|
+
attr_accessor :console_address
|
|
37
|
+
|
|
38
|
+
# Brand shown in the dashboard nav header. Override to white-label the console, e.g.
|
|
39
|
+
# +config.nav_title = 'Acme Support'+. Defaults to +'🥚 Protege'+.
|
|
40
|
+
attr_accessor :nav_title
|
|
41
|
+
|
|
42
|
+
# User-Agent the built-in web tools (+web_search+, +web_fetch+) send on outbound HTTP. Defaults to
|
|
43
|
+
# {DEFAULT_HTTP_USER_AGENT} — a generic browser string, not a Protege-branded one. Override to adopt
|
|
44
|
+
# your host machine's own browser agent, e.g.
|
|
45
|
+
# +config.http_user_agent = ENV.fetch('HTTP_USER_AGENT', Protege::Configuration::DEFAULT_HTTP_USER_AGENT)+.
|
|
46
|
+
attr_accessor :http_user_agent
|
|
47
|
+
|
|
48
|
+
# Sets the logger used throughout the engine. Override in an initializer to route Protege output
|
|
49
|
+
# to a dedicated log target, e.g. +config.logger = Logger.new(Rails.root.join('log/protege.log'))+.
|
|
50
|
+
attr_writer :logger
|
|
51
|
+
|
|
52
|
+
# 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
|
|
55
|
+
# initializer if a host groups its extensions elsewhere (e.g. +config.tools_path = 'app/agents/tools'+);
|
|
56
|
+
# 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
|
|
58
|
+
|
|
59
|
+
# Per-provider options, keyed by provider id — assign a plain Hash (see {#providers}).
|
|
60
|
+
attr_writer :providers
|
|
61
|
+
|
|
62
|
+
# The inbound attachment limits — assign a +Gateway.build_attachment_policy+.
|
|
63
|
+
attr_writer :attachment_policy
|
|
64
|
+
|
|
65
|
+
# Set the global inbound access policy — assign a +Gateway.build_access_policy+.
|
|
66
|
+
attr_writer :inbound_access
|
|
67
|
+
|
|
68
|
+
# Inference tracing options — assign a plain Hash, e.g. +config.tracing = { enabled: true }+ (see
|
|
69
|
+
# {#tracing}). Off by default; nothing is captured until a host opts in.
|
|
70
|
+
attr_writer :tracing
|
|
71
|
+
|
|
72
|
+
# Initialize configuration with engine defaults.
|
|
73
|
+
#
|
|
74
|
+
# @return [void]
|
|
75
|
+
def initialize
|
|
76
|
+
@nav_title = '🥚 Protege'
|
|
77
|
+
@console_address = 'console@protege.local'
|
|
78
|
+
@http_user_agent = DEFAULT_HTTP_USER_AGENT
|
|
79
|
+
@max_tool_turns = 8
|
|
80
|
+
@provider_id = nil
|
|
81
|
+
@logger = nil
|
|
82
|
+
@tools_path = 'app/tools'
|
|
83
|
+
@resolvers_path = 'app/resolvers'
|
|
84
|
+
@hooks_path = 'app/hooks'
|
|
85
|
+
@personas_path = 'app/personas'
|
|
86
|
+
@providers_path = 'app/providers'
|
|
87
|
+
@providers = nil
|
|
88
|
+
@attachment_policy = nil
|
|
89
|
+
@inbound_access = nil
|
|
90
|
+
@tracing = nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Return the configured logger, lazily resolving a default.
|
|
94
|
+
#
|
|
95
|
+
# Falls back to +Rails.logger+ when running inside a Rails application, or a plain stdout logger
|
|
96
|
+
# otherwise (useful in isolated tests and scripts).
|
|
97
|
+
#
|
|
98
|
+
# @return [Logger] the active logger
|
|
99
|
+
def logger
|
|
100
|
+
@logger ||= defined?(Rails) ? Rails.logger : Logger.new($stdout)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Per-provider options, a plain Hash keyed by provider id — the Rails-style config seam every
|
|
104
|
+
# provider (built-in or host-written) reads its settings from. Assigned wholesale in the initializer;
|
|
105
|
+
# assignment replaces the whole Hash, so include every provider you configure:
|
|
106
|
+
#
|
|
107
|
+
# config.providers = {
|
|
108
|
+
# openrouter: { api_key: ENV['OPENROUTER_API_KEY'], base_url: '...' },
|
|
109
|
+
# my_llm: { api_key: ENV['MY_LLM_KEY'], region: 'us' }
|
|
110
|
+
# }
|
|
111
|
+
#
|
|
112
|
+
# Defaults to +{ openrouter: {} }+ so the built-in is harmless when unused. A provider reads its own
|
|
113
|
+
# slice via {#provider_options} (keyed by its +protege_id+). Values are opaque to the engine — a
|
|
114
|
+
# host-written provider may put any keys it likes under its own id and read them back here.
|
|
115
|
+
#
|
|
116
|
+
# @return [Hash{Symbol=>Hash}] per-provider options keyed by provider id
|
|
117
|
+
def providers
|
|
118
|
+
@providers ||= { openrouter: {} }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The options Hash for one provider, by id — the seam a +Protege::Provider+ reads its settings from
|
|
122
|
+
# (typically +provider_options(self.class.id)+). Nil-safe: returns an empty Hash when the provider
|
|
123
|
+
# has no configured slice.
|
|
124
|
+
#
|
|
125
|
+
# @param id [Symbol] the provider id (its +protege_id+)
|
|
126
|
+
# @return [Hash] that provider's options, or +{}+ when unconfigured
|
|
127
|
+
def provider_options(id)
|
|
128
|
+
providers[id] || {}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The inbound attachment limits, assigned in the initializer via
|
|
132
|
+
# +config.attachment_policy = Gateway.build_attachment_policy(...)+. Defaults to the engine's
|
|
133
|
+
# standard limits (10 MB/attachment, 10/message, 25 MB total). Consulted by the inbound mailbox and
|
|
134
|
+
# the +send_email+ tool.
|
|
135
|
+
#
|
|
136
|
+
# @return [Protege::Gateway::AttachmentPolicy] the attachment policy
|
|
137
|
+
def attachment_policy
|
|
138
|
+
@attachment_policy ||= Gateway.build_attachment_policy
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# 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
|
|
144
|
+
# (each layer can only narrow, never widen).
|
|
145
|
+
#
|
|
146
|
+
# Defaults to a bare permit-everyone policy, built lazily on first read so an unconfigured engine
|
|
147
|
+
# imposes no constraint.
|
|
148
|
+
#
|
|
149
|
+
# @example Restrict every persona to the company domain
|
|
150
|
+
# config.inbound_access = Protege::Gateway.build_access_policy(allow: ['*@company.co'])
|
|
151
|
+
#
|
|
152
|
+
# @return [Protege::Gateway::AccessPolicy] the configured policy, or a permit-everyone default
|
|
153
|
+
def inbound_access
|
|
154
|
+
@inbound_access || Gateway.build_access_policy
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Inference tracing options, a plain Hash — the on/off seam for durable turn snapshots. Assigned
|
|
158
|
+
# wholesale in the initializer (like {#providers}); assignment replaces the whole Hash. Defaults to
|
|
159
|
+
# +{ enabled: false }+, so an unconfigured engine records nothing. When +enabled+ is truthy, the
|
|
160
|
+
# harness emits a per-turn +InferenceGeneratedEvent+ that the tracing subscriber snapshots into
|
|
161
|
+
# +Protege::Trace+ rows.
|
|
162
|
+
#
|
|
163
|
+
# @example Turn tracing on
|
|
164
|
+
# config.tracing = { enabled: true }
|
|
165
|
+
#
|
|
166
|
+
# @return [Hash] the tracing options, defaulting to +{ enabled: false }+
|
|
167
|
+
def tracing
|
|
168
|
+
@tracing ||= { enabled: false }
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails'
|
|
4
|
+
|
|
5
|
+
# Rails-layer runtime dependencies used by the engine's +app/+ (views, controllers, helpers): Slim
|
|
6
|
+
# templating for the +.slim+ views, and Pagy for dashboard pagination. Required here, with the Engine,
|
|
7
|
+
# rather than in +protege.rb+ (which loads only the plain-Ruby core entry points).
|
|
8
|
+
require 'slim-rails'
|
|
9
|
+
require 'pagy'
|
|
10
|
+
require 'pagy/extras/overflow'
|
|
11
|
+
|
|
12
|
+
module Protege
|
|
13
|
+
# Rails engine that mounts Protege into a host application. This is the boot seam of the LOGI
|
|
14
|
+
# pipeline: it wires inbound email to the Gateway via Action Mailbox routing, exposes the engine's
|
|
15
|
+
# +lib/+ to autoloading so Orchestrator/Inference constructs resolve lazily, shares route helpers
|
|
16
|
+
# into the host's view context for Turbo broadcasts, and installs the inference stream broadcaster
|
|
17
|
+
# and engine migrations. It runs an isolated +Protege+ namespace so host and engine constants never
|
|
18
|
+
# collide.
|
|
19
|
+
class Engine < ::Rails::Engine
|
|
20
|
+
isolate_namespace Protege
|
|
21
|
+
|
|
22
|
+
# Expose engine lib/ to Rails autoloading (Harness, Provider, etc.). lib/ is autoload-only — not
|
|
23
|
+
# eager loaded — so Zeitwerk never tries to map filenames like version.rb to class constants.
|
|
24
|
+
config.autoload_paths << File.expand_path('..', __dir__)
|
|
25
|
+
|
|
26
|
+
# Add engine migrations to the host app so bin/rails db:migrate picks them up.
|
|
27
|
+
initializer 'protege.migrations' do |app|
|
|
28
|
+
config.paths['db/migrate'].expanded.each do |path|
|
|
29
|
+
app.config.paths['db/migrate'] << path
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Clamp an out-of-range +?page=+ to the last page rather than raising +Pagy::OverflowError+ (via the
|
|
34
|
+
# +overflow+ extra required above), so a stale or hand-typed page number degrades gracefully. We use
|
|
35
|
+
# only Pagy's lightweight metadata object and render our own sidebar footer markup.
|
|
36
|
+
initializer 'protege.pagy' do
|
|
37
|
+
Pagy::DEFAULT[:overflow] = :last_page
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Keep non-autoloadable lib/ subdirs out of Zeitwerk. Generators are loaded by Rails' generator
|
|
41
|
+
# system (and their +Protege::Generators::+ constants don't match the lib-rooted path Zeitwerk would
|
|
42
|
+
# expect); tasks are .rake. Apps would use +config.autoload_lib(ignore:)+, but that's unavailable
|
|
43
|
+
# for engines, so ignore explicitly — the form the autoloading guide documents for this case.
|
|
44
|
+
initializer 'protege.ignore_lib_non_autoloadable', before: :setup_main_autoloader do
|
|
45
|
+
Rails.autoloaders.main.ignore(lib_path('generators'),
|
|
46
|
+
lib_path('tasks'))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Collapse the directories that merely group top-level constants so their files resolve to flat
|
|
50
|
+
# constants without a namespace segment: +errors/timeout_error.rb+ → +Protege::TimeoutError+ (not
|
|
51
|
+
# +Protege::Errors::TimeoutError+), +types/result.rb+ → +Protege::Result+, +extensions/tool.rb+ →
|
|
52
|
+
# +Protege::Tool+, +events/inference_started_event.rb+ → +Protege::InferenceStartedEvent+. The
|
|
53
|
+
# directories organize the files; +collapse+ keeps the constants flat.
|
|
54
|
+
initializer 'protege.collapse_shared', before: :setup_main_autoloader do
|
|
55
|
+
Rails.autoloaders.main.collapse lib_path('protege/errors')
|
|
56
|
+
Rails.autoloaders.main.collapse lib_path('protege/events')
|
|
57
|
+
Rails.autoloaders.main.collapse lib_path('protege/extensions')
|
|
58
|
+
Rails.autoloaders.main.collapse lib_path('protege/types')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Route inbound Action Mailbox email addressed to a Protege domain to Protege::AgentMailbox — the
|
|
62
|
+
# Gateway's inbound edge. Scoped by +Gateway.claims?+ (matches a registered +EmailDomain+) rather
|
|
63
|
+
# than a catch-all, so mounting Protege into a host that also uses Action Mailbox never hijacks the
|
|
64
|
+
# host's other inbound mail; non-Protege mail falls through to the host's own routing.
|
|
65
|
+
initializer 'protege.action_mailbox_routing' do
|
|
66
|
+
ActiveSupport.on_load(:action_mailbox) do
|
|
67
|
+
routing ->(inbound_email) { Protege::Gateway.claims?(inbound_email:) } => 'protege/agent'
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Add the engine's route helpers to the host app's view context so partials rendered outside a request
|
|
72
|
+
# (e.g. Turbo broadcasts via ApplicationController.render) can resolve helpers like +thread_path+ without
|
|
73
|
+
# explicit host-app configuration.
|
|
74
|
+
initializer 'protege.add_route_helpers' do
|
|
75
|
+
ActiveSupport.on_load(:action_controller) do
|
|
76
|
+
helper Protege::Engine.routes.url_helpers
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Expose engine route helpers to all controllers and view contexts once routes are fully
|
|
81
|
+
# initialised. This ensures partials rendered outside a request (e.g. Turbo broadcasts via
|
|
82
|
+
# ApplicationController.render) can resolve helpers like +thread_path+ without explicit host-app
|
|
83
|
+
# configuration, and installs the engine's event subscribers (all under +Protege::Subscribers+): the
|
|
84
|
+
# introspection broadcaster (console activity feed), the hook dispatcher (fans events to host hooks —
|
|
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.
|
|
87
|
+
config.after_initialize do
|
|
88
|
+
Protege::IntrospectionBroadcaster.install!
|
|
89
|
+
Protege::Subscribers::HookDispatcher.install!
|
|
90
|
+
Protege::Subscribers::Tracing.install!
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Return the absolute path to a file in the engine's lib/ directory. Used by the engine's rake tasks
|
|
94
|
+
# to locate templates and other static assets.
|
|
95
|
+
#
|
|
96
|
+
# @param rel_path [String] the relative path from lib/
|
|
97
|
+
# @return [String]
|
|
98
|
+
def lib_path(rel_path)
|
|
99
|
+
lib = File.expand_path('..', __dir__)
|
|
100
|
+
File.join(lib, rel_path)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Base class for every Protege error. Rescue +Protege::Error+ to catch anything the engine raises
|
|
5
|
+
# without knowing the specific subclass; the concrete errors are flat top-level constants
|
|
6
|
+
# (+Protege::TimeoutError+, +Protege::ToolNotFoundError+, …) defined in +lib/protege/errors/+ (a
|
|
7
|
+
# collapsed autoload dir, so they read as +Protege::+ rather than +Protege::Errors::+).
|
|
8
|
+
#
|
|
9
|
+
# Errors are ownerless, pipeline-wide types, so they live at the top level (next to +Result+ and
|
|
10
|
+
# +Event+) rather than inside the Loop/Orchestrator/Gateway/Inference behavior namespaces.
|
|
11
|
+
class Error < StandardError
|
|
12
|
+
# Provider failures a later attempt may clear — a job's +retry_on+ list.
|
|
13
|
+
TRANSIENT = [
|
|
14
|
+
InternalError,
|
|
15
|
+
RateLimitedError,
|
|
16
|
+
TimeoutError,
|
|
17
|
+
UnavailableError
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
# Provider failures retrying cannot help — a job's +discard_on+ list.
|
|
21
|
+
PERMANENT = [
|
|
22
|
+
BadRequestError,
|
|
23
|
+
ContractViolationError,
|
|
24
|
+
InvalidModelError,
|
|
25
|
+
ResponseParseFailedError,
|
|
26
|
+
UnauthorizedError,
|
|
27
|
+
UnsupportedCapabilityError
|
|
28
|
+
].freeze
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Wraps a plain-string reason so callers that expect an Exception on +Result#error+ always get one.
|
|
5
|
+
# Built automatically by +Result.failure+ when handed a String rather than an Exception.
|
|
6
|
+
class GenericError < Error; end
|
|
7
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# A tool's +#use+ returned something other than a +Protege::Result+. Tools must build their return
|
|
5
|
+
# value via +Result.success+ / +Result.failure+.
|
|
6
|
+
class InvalidToolResultError < Error
|
|
7
|
+
def initialize(tool:, got:) = super("#{tool}#use must return a Protege::Result, got #{got}")
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# A +LoadFileResolver+'s configured path does not exist — a developer misconfiguration (the path is
|
|
5
|
+
# author-supplied), so we fail loudly rather than silently contributing nothing.
|
|
6
|
+
class ResolverFileNotFoundError < Error
|
|
7
|
+
def initialize(path:, absolute:) = super("no file at #{absolute} (from path #{path.inspect})")
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
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.
|
|
9
|
+
class ToolNotAvailableError < Error
|
|
10
|
+
def initialize(name:) = super("tool :#{name} is not available to this persona")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# The model requested a tool whose name is not in +ToolMixin.registered+. Distinct from runtime
|
|
5
|
+
# errors raised inside +#use+, which the harness wraps into the tool result so the model self-corrects.
|
|
6
|
+
class ToolNotFoundError < Error
|
|
7
|
+
def initialize(name:) = super("no tool registered for :#{name}")
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# the provider/model does not support the requested capability
|
|
5
|
+
class UnsupportedCapabilityError < Error
|
|
6
|
+
def initialize(detail = nil) = super(detail || 'the provider/model does not support the requested capability')
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Base class for every Protege event — both the typed, nil-safe wrapper handed to subscribers and the
|
|
5
|
+
# class-level publish/subscribe surface. Concrete events are one flat class each (+InferenceStartedEvent+,
|
|
6
|
+
# +ToolCallCompletedEvent+, …) in +lib/protege/events/+ (a collapsed autoload dir, so they read as
|
|
7
|
+
# +Protege::+ rather than +Protege::Events::+), mirroring the +Protege::*Error+ hierarchy.
|
|
8
|
+
#
|
|
9
|
+
# Each subclass declares its +ActiveSupport::Notifications+ topic with the +channel+ class macro (the
|
|
10
|
+
# same class-attribute style as a provider's +protege_id+) and writes a plain reader per payload key.
|
|
11
|
+
# Publishing and subscribing go through the subclass:
|
|
12
|
+
#
|
|
13
|
+
# Protege::InferenceCompletedEvent.emit(persona:, message:, result:) # engine internals
|
|
14
|
+
# Protege::InferenceCompletedEvent.subscribe { |event| event.result } # host / broadcaster
|
|
15
|
+
#
|
|
16
|
+
# +correlation_id+ is injected automatically from +Protege::Current+ into every emitted event — set it
|
|
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
|
|
19
|
+
# with named, nil-safe accessors (and +event[:key]+ reaches anything without a dedicated reader).
|
|
20
|
+
class Event
|
|
21
|
+
# @return [Hash] the underlying payload hash
|
|
22
|
+
attr_reader :payload
|
|
23
|
+
|
|
24
|
+
# Wrap a raw notification payload; frozen so subscribers can't mutate shared state.
|
|
25
|
+
#
|
|
26
|
+
# @param payload [Hash] the event payload as published by {emit}
|
|
27
|
+
# @return [void]
|
|
28
|
+
def initialize(payload)
|
|
29
|
+
@payload = payload
|
|
30
|
+
freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [String, nil] the per-run correlation id injected by {emit}
|
|
34
|
+
def correlation_id = @payload[:correlation_id]
|
|
35
|
+
|
|
36
|
+
# Read an arbitrary payload key not covered by a named reader.
|
|
37
|
+
#
|
|
38
|
+
# @param key [Symbol] the payload key
|
|
39
|
+
# @return [Object, nil] the value, or nil when absent
|
|
40
|
+
def [](key) = @payload[key]
|
|
41
|
+
|
|
42
|
+
class << self
|
|
43
|
+
# Declare (with an argument) or read (without) this event's +ActiveSupport::Notifications+ topic.
|
|
44
|
+
# Called in the subclass body — the same class-attribute idiom as a provider's +protege_id+.
|
|
45
|
+
#
|
|
46
|
+
# @param value [String, nil] the topic to set, or nil to read the declared one
|
|
47
|
+
# @return [String] the topic
|
|
48
|
+
def channel(value = nil)
|
|
49
|
+
value ? @channel = value : @channel
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Publish this event, injecting +correlation_id+ from +Protege::Current+ so callers never pass it.
|
|
53
|
+
#
|
|
54
|
+
# @param payload [Hash] the event's payload keyed by symbol
|
|
55
|
+
# @return [void]
|
|
56
|
+
def emit(**payload)
|
|
57
|
+
ActiveSupport::Notifications.instrument(channel,
|
|
58
|
+
correlation_id: Protege::Current.correlation_id,
|
|
59
|
+
**payload)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Subscribe to this event, yielding a typed instance of this class (not the raw payload hash).
|
|
63
|
+
#
|
|
64
|
+
# @yieldparam event [Protege::Event] the wrapped event
|
|
65
|
+
# @return [Object] the subscription handle returned by +ActiveSupport::Notifications+
|
|
66
|
+
def subscribe(&block)
|
|
67
|
+
ActiveSupport::Notifications.subscribe(channel) do |_name, _start, _finish, _id, payload|
|
|
68
|
+
block.call new(payload)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Every concrete event class, in fire order. Built lazily on first call (at boot, by
|
|
73
|
+
# +Subscribers::HookDispatcher.install!+), so each subclass is autoloaded only when referenced
|
|
74
|
+
# here — after this base and its +channel+ macro are defined — keeping +lib/+ autoload-only with
|
|
75
|
+
# no load-order constraint.
|
|
76
|
+
#
|
|
77
|
+
# @return [Array<Class>] the event classes
|
|
78
|
+
def all
|
|
79
|
+
@all ||= [
|
|
80
|
+
InferenceStartedEvent,
|
|
81
|
+
InferenceGeneratedEvent,
|
|
82
|
+
InferenceCompletedEvent,
|
|
83
|
+
InferenceFailedEvent,
|
|
84
|
+
ToolCallsReceivedEvent,
|
|
85
|
+
ToolCallStartedEvent,
|
|
86
|
+
ToolCallCompletedEvent,
|
|
87
|
+
ToolCallFailedEvent,
|
|
88
|
+
InferenceMaxTurnsReachedEvent,
|
|
89
|
+
InferenceChunkEvent,
|
|
90
|
+
LoopRunEnqueuedEvent,
|
|
91
|
+
LoopRunStartedEvent,
|
|
92
|
+
LoopRunCompletedEvent,
|
|
93
|
+
LoopRunFailedEvent
|
|
94
|
+
].freeze
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Fired for each token chunk during streaming inference.
|
|
5
|
+
class InferenceChunkEvent < Event
|
|
6
|
+
channel 'protege.inference.chunk'
|
|
7
|
+
|
|
8
|
+
def persona = self[:persona]
|
|
9
|
+
def thread = self[:thread]
|
|
10
|
+
def chunk = self[:chunk]
|
|
11
|
+
|
|
12
|
+
# The decoded text of the streamed chunk, if any.
|
|
13
|
+
#
|
|
14
|
+
# @return [String, nil] the chunk's +:content+ as a string, or nil when there is no chunk
|
|
15
|
+
def chunk_content = chunk && chunk[:content].to_s
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Fired after the tool loop returns a final, tool-free response.
|
|
5
|
+
class InferenceCompletedEvent < Event
|
|
6
|
+
channel 'protege.inference.completed'
|
|
7
|
+
|
|
8
|
+
def persona = self[:persona]
|
|
9
|
+
def message = self[:message]
|
|
10
|
+
def result = self[:result]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Fired when a run raises before completing.
|
|
5
|
+
class InferenceFailedEvent < Event
|
|
6
|
+
channel 'protege.inference.failed'
|
|
7
|
+
|
|
8
|
+
def persona = self[:persona]
|
|
9
|
+
def message = self[:message]
|
|
10
|
+
def error = self[:error]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Fired after every single provider generation within the tool loop — once per turn, including the
|
|
5
|
+
# final tool-free turn. Unlike +InferenceCompletedEvent+ (one per run, carrying only the final
|
|
6
|
+
# result), this carries the wire +request+ as sent, the +response+ it produced, and the inference
|
|
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;
|
|
9
|
+
# +correlation_id+ (from +Protege::Current+) and +turn+ group the turns of one run for dedup.
|
|
10
|
+
class InferenceGeneratedEvent < Event
|
|
11
|
+
channel 'protege.inference.generated'
|
|
12
|
+
|
|
13
|
+
def turn = self[:turn]
|
|
14
|
+
def request = self[:request]
|
|
15
|
+
def response = self[:response]
|
|
16
|
+
def model = self[:model]
|
|
17
|
+
def settings = self[:settings]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Fired when the tool loop hits the max turn count without completing.
|
|
5
|
+
class InferenceMaxTurnsReachedEvent < Event
|
|
6
|
+
channel 'protege.inference.max_turns_reached'
|
|
7
|
+
|
|
8
|
+
def persona = self[:persona]
|
|
9
|
+
def turn = self[:turn]
|
|
10
|
+
def tool_calls = self[:tool_calls]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Fired at the start of the tool loop, before the first provider call.
|
|
5
|
+
class InferenceStartedEvent < Event
|
|
6
|
+
channel 'protege.inference.started'
|
|
7
|
+
|
|
8
|
+
def persona = self[:persona]
|
|
9
|
+
def message = self[:message]
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Fired when a scheduled responsibility run completes successfully (the Loop layer).
|
|
5
|
+
class LoopRunCompletedEvent < Event
|
|
6
|
+
channel 'protege.loop.run.completed'
|
|
7
|
+
|
|
8
|
+
def name = self[:name]
|
|
9
|
+
def persona_name = self[:persona_name]
|
|
10
|
+
end
|
|
11
|
+
end
|