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,305 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Protege
|
|
6
|
+
module Orchestrator
|
|
7
|
+
# Multi-turn inference runner — the heart of the LOGI Orchestrator. Generates against the
|
|
8
|
+
# configured provider and dispatches tool calls until the model returns a tool-free response or
|
|
9
|
+
# +max_tool_turns+ is reached.
|
|
10
|
+
#
|
|
11
|
+
# This is the **abstract base** that owns the shared algorithm (event-wrapped tool loop, provider
|
|
12
|
+
# generation, tool dispatch, attachment review). What *drives* a run is left to subclasses, which
|
|
13
|
+
# each define one clean path: +ReplyHarness+ for a reactive run (an inbound email) and
|
|
14
|
+
# +ResponsibilityHarness+ for a proactive one (a scheduled Loop responsibility). The branch between
|
|
15
|
+
# them lives at the job boundary — the caller picks the subclass — so there are no run-kind
|
|
16
|
+
# conditionals in this loop. Subclasses fill the hooks: +#resolvers+, +#user_content+, +#context+,
|
|
17
|
+
# and (reply only) +#message+ / +#streaming?+.
|
|
18
|
+
#
|
|
19
|
+
# +Protege::Current.correlation_id+ must be set before calling so that emitted events carry the id
|
|
20
|
+
# automatically. Collaborates with +Inference+ (request building and generation), +Tool+ (dispatch
|
|
21
|
+
# and registry), the persona's +ResolverChain+ (context assembly), and the +Protege::*Event+ classes
|
|
22
|
+
# (observability).
|
|
23
|
+
#
|
|
24
|
+
# @example
|
|
25
|
+
# ReplyHarness.run(persona:, message:)
|
|
26
|
+
# ResponsibilityHarness.run(persona:, responsibility:)
|
|
27
|
+
#
|
|
28
|
+
# == Lifecycle
|
|
29
|
+
#
|
|
30
|
+
# 1. Emit +inference_started+ event.
|
|
31
|
+
# 2. Build the initial +Provider::Request+ from the opening user turn, registered tools, and
|
|
32
|
+
# resolver-contributed context messages.
|
|
33
|
+
# 3. Enter the tool loop — generate, dispatch any tool calls, extend the request with results,
|
|
34
|
+
# repeat until the model returns no tool calls or +max_tool_turns+ is exhausted.
|
|
35
|
+
# 4. Emit +inference_completed+ event and return the final response.
|
|
36
|
+
class Harness
|
|
37
|
+
class << self
|
|
38
|
+
# Instantiate the concrete harness and run it in one call. Keyword arguments are forwarded to
|
|
39
|
+
# the subclass initializer (e.g. +message:+ for +ReplyHarness+, +responsibility:+ for
|
|
40
|
+
# +ResponsibilityHarness+).
|
|
41
|
+
#
|
|
42
|
+
# @return [Object] the final +Provider::Response+
|
|
43
|
+
def run(**)
|
|
44
|
+
new(**).run
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param persona [Protege::Persona] the persona handling the run
|
|
49
|
+
# @return [void]
|
|
50
|
+
def initialize(persona:)
|
|
51
|
+
@persona = persona
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Entry point. Emit lifecycle events around the tool loop.
|
|
55
|
+
#
|
|
56
|
+
# Always emits a terminal event — +inference_completed+ on success or +inference_failed+ when the
|
|
57
|
+
# loop raises — so subscribers (and stateful hooks keyed by correlation id) are never left
|
|
58
|
+
# waiting on a completion that never comes. The error is re-raised so the job's retry/discard
|
|
59
|
+
# policy still applies.
|
|
60
|
+
#
|
|
61
|
+
# @return [Object] the final +Provider::Response+
|
|
62
|
+
# @raise [StandardError] re-raised after +inference_failed+ is emitted
|
|
63
|
+
def run
|
|
64
|
+
InferenceStartedEvent.emit(persona:, message:)
|
|
65
|
+
|
|
66
|
+
result = run_tool_loop
|
|
67
|
+
|
|
68
|
+
InferenceCompletedEvent.emit(persona:, message:, result:)
|
|
69
|
+
|
|
70
|
+
result
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
InferenceFailedEvent.emit(persona:, message:, error: e)
|
|
73
|
+
raise
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# @return [Protege::Persona] the persona handling the run
|
|
79
|
+
attr_reader :persona
|
|
80
|
+
|
|
81
|
+
# ── Kind-specific hooks (subclasses override) ─────────────────────────────
|
|
82
|
+
|
|
83
|
+
# The resolver chain that assembles this run's context messages.
|
|
84
|
+
# @return [Orchestrator::ResolverChain]
|
|
85
|
+
def resolvers
|
|
86
|
+
raise NotImplementedError, "#{self.class} must implement #resolvers"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The per-run context shared by every tool and resolver invocation.
|
|
90
|
+
# @return [Orchestrator::Context]
|
|
91
|
+
def context
|
|
92
|
+
raise NotImplementedError, "#{self.class} must implement #context"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# An opening user turn for the harness to append, when a run carries a *live* trigger that isn't
|
|
96
|
+
# a stored record — the inbound email body for a reply. Defaults to none: assembling context
|
|
97
|
+
# (system prompts, history, and a scheduled run's instructions) is the resolver chain's job, not
|
|
98
|
+
# the harness's, so a subclass overrides this only for a live trigger it must inject itself.
|
|
99
|
+
#
|
|
100
|
+
# @return [String, Array<Object>, nil] the user-turn content, or nil to add nothing
|
|
101
|
+
def user_content
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The inbound message, when this run answers one. Reactive runs override it; for scheduled runs
|
|
106
|
+
# it stays nil (events carry a nil message, and there is nothing to stream).
|
|
107
|
+
# @return [Object, nil]
|
|
108
|
+
def message
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Whether to stream tokens to a connected console. Only the reactive console path streams.
|
|
113
|
+
# @return [Boolean]
|
|
114
|
+
def streaming?
|
|
115
|
+
false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# ── Shared loop ───────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
# Run the core generate → dispatch → extend loop.
|
|
121
|
+
#
|
|
122
|
+
# Runs up to +max_loop_iterations+ turns. Returns early when the model signals completion (no
|
|
123
|
+
# tool calls). On the final allowed turn, emits +max_turns_reached+ and breaks, returning
|
|
124
|
+
# whatever response the model last produced.
|
|
125
|
+
#
|
|
126
|
+
# @return [Object] the final +Provider::Response+
|
|
127
|
+
def run_tool_loop
|
|
128
|
+
final = nil
|
|
129
|
+
extended = build_initial_request
|
|
130
|
+
|
|
131
|
+
max_loop_iterations.times do |turn|
|
|
132
|
+
final = capture(turn:, request: extended) { run_generation(extended) }
|
|
133
|
+
|
|
134
|
+
return final if final.tool_calls.blank?
|
|
135
|
+
|
|
136
|
+
if turn >= (max_loop_iterations - 1)
|
|
137
|
+
InferenceMaxTurnsReachedEvent.emit(persona:, turn:, tool_calls: final.tool_calls)
|
|
138
|
+
break
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
ToolCallsReceivedEvent.emit(persona:, tool_calls: final.tool_calls)
|
|
142
|
+
|
|
143
|
+
tool_results = final.tool_calls.map { invoke_tool(_1) }
|
|
144
|
+
record_tool_round(turn:, response: final, tool_results:)
|
|
145
|
+
extended = Inference.extend_request(
|
|
146
|
+
previous: extended,
|
|
147
|
+
response: final,
|
|
148
|
+
tool_results:,
|
|
149
|
+
review_parts: review_parts_for(tool_results)
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
final
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Wrap one turn's generation for tracing. When tracing is off (the default) this is a bare
|
|
157
|
+
# pass-through — it yields and returns the response with zero overhead, publishing nothing. When
|
|
158
|
+
# tracing is on, it emits +InferenceGeneratedEvent+ (the seam the tracing subscriber snapshots).
|
|
159
|
+
# Gating the *emission* here — rather than always emitting and letting the subscriber decide —
|
|
160
|
+
# means the (potentially large) request payload is never assembled into an event unless a host
|
|
161
|
+
# opted in.
|
|
162
|
+
#
|
|
163
|
+
# @param turn [Integer] this generation's turn index within the run
|
|
164
|
+
# @param request [Object] the provider request being generated against
|
|
165
|
+
# @yieldreturn [Object] the provider response
|
|
166
|
+
# @return [Object] the provider response
|
|
167
|
+
def capture(turn:, request:)
|
|
168
|
+
return yield unless Protege.configuration.tracing[:enabled]
|
|
169
|
+
|
|
170
|
+
response = yield
|
|
171
|
+
emit_generated(turn:, request:, response:)
|
|
172
|
+
response
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Emit the per-turn trace event: the wire request (+Request#to_wire+ — the very serialization the
|
|
176
|
+
# API call uses, so the snapshot is exact), the response, and the inference settings that shaped it
|
|
177
|
+
# (+model+ + +settings+, pulled from the provider the harness resolves — not global config — so a
|
|
178
|
+
# future persona-level override is captured). Best-effort: any failure is logged and swallowed, so
|
|
179
|
+
# building or publishing the event can never abort a run whose side effects may already have run.
|
|
180
|
+
#
|
|
181
|
+
# @param turn [Integer] this generation's turn index
|
|
182
|
+
# @param request [Object] the provider request that was sent
|
|
183
|
+
# @param response [Object] the provider response it produced
|
|
184
|
+
# @return [void]
|
|
185
|
+
def emit_generated(turn:, request:, response:)
|
|
186
|
+
provider = Inference.provider
|
|
187
|
+
|
|
188
|
+
InferenceGeneratedEvent.emit(
|
|
189
|
+
turn:,
|
|
190
|
+
request: request.to_wire,
|
|
191
|
+
response: response.to_wire,
|
|
192
|
+
model: provider.model,
|
|
193
|
+
settings: provider.settings
|
|
194
|
+
)
|
|
195
|
+
rescue StandardError => e
|
|
196
|
+
Protege.configuration.logger.error(
|
|
197
|
+
"[Protege] failed to emit trace event (turn #{turn}): #{e.class}: #{e.message}"
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Run a single generation, streaming chunks to the browser when this run streams (console only).
|
|
202
|
+
#
|
|
203
|
+
# @param request [Object] the provider request to generate against
|
|
204
|
+
# @return [Object] the provider response
|
|
205
|
+
def run_generation(request)
|
|
206
|
+
if streaming?
|
|
207
|
+
Inference.generate_stream(request) do |chunk|
|
|
208
|
+
InferenceChunkEvent.emit(persona:, thread: message.email_thread, chunk:)
|
|
209
|
+
end
|
|
210
|
+
else
|
|
211
|
+
Inference.generate(request)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Compute the loop bound. +max_tool_turns+ is how many tool-calling rounds to allow; the loop
|
|
216
|
+
# needs one extra iteration for the final tool-free generation.
|
|
217
|
+
#
|
|
218
|
+
# @return [Integer] the maximum number of loop iterations
|
|
219
|
+
def max_loop_iterations
|
|
220
|
+
Protege.configuration.max_tool_turns + 1
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Record one completed tool-calling round to the run's durable tool history.
|
|
224
|
+
#
|
|
225
|
+
# Recording is **best-effort**: it is an audit/memory side effect, not the run's mission, so any
|
|
226
|
+
# failure here is logged and swallowed. It must never abort the tool loop — the tools have already
|
|
227
|
+
# executed (a reply may have already been sent), and letting a persistence hiccup raise would kill
|
|
228
|
+
# the run and, under retry, re-execute those side effects. The actual write is delegated to
|
|
229
|
+
# +#persist_tool_round+, which subclasses override.
|
|
230
|
+
#
|
|
231
|
+
# @param turn [Integer] the tool-calling round index within this run
|
|
232
|
+
# @param response [Object] the assistant response carrying this round's tool calls
|
|
233
|
+
# @param tool_results [Array<Protege::ToolResult>] the paired results
|
|
234
|
+
# @return [void]
|
|
235
|
+
def record_tool_round(turn:, response:, tool_results:)
|
|
236
|
+
persist_tool_round(turn:, response:, tool_results:)
|
|
237
|
+
rescue StandardError => e
|
|
238
|
+
Protege.configuration.logger.error(
|
|
239
|
+
"[Protege] failed to record tool use (turn #{turn}): #{e.class}: #{e.message}"
|
|
240
|
+
)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Persist this round's tool uses. Subclass hook — the base records nothing (a plain run keeps no
|
|
244
|
+
# tool history). +ReplyHarness+ and +ResponsibilityHarness+ override it.
|
|
245
|
+
#
|
|
246
|
+
# @param turn [Integer] the tool-calling round index within this run
|
|
247
|
+
# @param response [Object] the assistant response carrying this round's tool calls
|
|
248
|
+
# @param tool_results [Array<Protege::ToolResult>] the paired results
|
|
249
|
+
# @return [void]
|
|
250
|
+
def persist_tool_round(turn:, response:, tool_results:); end
|
|
251
|
+
|
|
252
|
+
# Dispatch a single tool call and emit the matching success/failure event.
|
|
253
|
+
#
|
|
254
|
+
# @param tool_call [Object] the tool call to dispatch
|
|
255
|
+
# @return [Protege::ToolResult] the call paired with its outcome
|
|
256
|
+
def invoke_tool(tool_call)
|
|
257
|
+
ToolCallStartedEvent.emit(persona:, tool_call:)
|
|
258
|
+
|
|
259
|
+
tool_result = ToolMixin.invoke(tool_call, context:)
|
|
260
|
+
|
|
261
|
+
if tool_result.result.success?
|
|
262
|
+
ToolCallCompletedEvent.emit(persona:, tool_call:, result: tool_result.result)
|
|
263
|
+
else
|
|
264
|
+
ToolCallFailedEvent.emit(persona:, tool_call:, error: tool_result.result.error)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
tool_result
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Collect the vision parts for any tool result that asked to show the model a file (the
|
|
271
|
+
# +read_attachment+ tool on an image/PDF). The result carries only the blob id — never the bytes —
|
|
272
|
+
# so the part is rebuilt here by re-resolving the blob, keeping binary out of the serialized tool
|
|
273
|
+
# text. These are appended as a trailing user turn by +extend_request+.
|
|
274
|
+
#
|
|
275
|
+
# @param tool_results [Array<Protege::ToolResult>] the turn's tool results
|
|
276
|
+
# @return [Array<Object>] the vision parts to show the model
|
|
277
|
+
def review_parts_for(tool_results)
|
|
278
|
+
tool_results.filter_map do |tool_result|
|
|
279
|
+
data = tool_result.result.data
|
|
280
|
+
next unless data[:review]
|
|
281
|
+
|
|
282
|
+
file = Protege::StoredFile.find(data[:blob_id])
|
|
283
|
+
file&.to_part
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Assemble the first +Provider::Request+ from the opening user turn, the persona's effective
|
|
288
|
+
# (scoped) tool set, inference config, and any context messages contributed by resolvers.
|
|
289
|
+
#
|
|
290
|
+
# The catalogue is +persona.available_tools+ rather than the whole registry, so a persona's
|
|
291
|
+
# code-declared grant and the operator's +disabled_tool_ids+ override bound what the model is
|
|
292
|
+
# offered — on both the reply and the scheduled path, since both inherit this base method. Later
|
|
293
|
+
# turns re-use +previous.tools+ (see +Inference.extend_request+), so the scope holds for the run.
|
|
294
|
+
#
|
|
295
|
+
# @return [Object] the initial provider request
|
|
296
|
+
def build_initial_request
|
|
297
|
+
Inference.build_request(
|
|
298
|
+
user_content:,
|
|
299
|
+
tools: persona.available_tools,
|
|
300
|
+
context_messages: resolvers.run(context)
|
|
301
|
+
)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
module Orchestrator
|
|
5
|
+
# Context for a reactive run — one driven by an inbound email. Adds the +message+ to the shared
|
|
6
|
+
# +Context+ surface so tools and resolvers can reach the email being answered (its thread, sender,
|
|
7
|
+
# body, attachments). The +#responsibility+ accessor inherited from +Context+ stays nil.
|
|
8
|
+
class ReplyContext < Context
|
|
9
|
+
# @return [Object] the inbound message being processed
|
|
10
|
+
attr_reader :message
|
|
11
|
+
|
|
12
|
+
# @param persona [Protege::Persona] the persona handling the run
|
|
13
|
+
# @param message [Object] the inbound message being processed
|
|
14
|
+
# @return [void]
|
|
15
|
+
def initialize(persona:, message:)
|
|
16
|
+
@message = message
|
|
17
|
+
super(persona:)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
module Orchestrator
|
|
5
|
+
# The reactive run — answering one inbound email. Runs the receiving persona's +message_resolvers+
|
|
6
|
+
# chain, opens the turn with the message body (plus any viewable attachments as vision parts), and
|
|
7
|
+
# streams tokens when the message came from the console. The +Harness+ base owns the tool loop;
|
|
8
|
+
# this subclass only supplies the reply-specific hooks.
|
|
9
|
+
class ReplyHarness < Harness
|
|
10
|
+
# @param persona [Protege::Persona] the persona that received the message
|
|
11
|
+
# @param message [Object] the inbound message to respond to
|
|
12
|
+
# @return [void]
|
|
13
|
+
def initialize(persona:, message:)
|
|
14
|
+
super(persona:)
|
|
15
|
+
@message = message
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# @return [Object] the inbound message being answered (overrides the base nil)
|
|
21
|
+
attr_reader :message
|
|
22
|
+
|
|
23
|
+
# @return [Orchestrator::ResolverChain] the persona's reply chain
|
|
24
|
+
def resolvers
|
|
25
|
+
persona.message_resolvers
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [String, Array<Object>] the message body, or body-plus-attachment parts
|
|
29
|
+
def user_content
|
|
30
|
+
message.content_parts
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Orchestrator::ReplyContext] the shared per-run context carrying the message
|
|
34
|
+
def context
|
|
35
|
+
@context ||= ReplyContext.new(persona:, message:)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Persist this tool-calling round as +ToolUse+ rows owned by the inbound message, so a later turn's
|
|
39
|
+
# +ThreadHistoryResolver+ can replay the agent's prior tool calls and results instead of losing
|
|
40
|
+
# them. Grouped by +correlation_id+ (this run) and +turn+; each call is paired with its result. A
|
|
41
|
+
# reply always carries a correlation id (a +Harness+ precondition), so it is not guarded here.
|
|
42
|
+
#
|
|
43
|
+
# @param turn [Integer] the tool-calling round index within this run
|
|
44
|
+
# @param response [Object] the assistant response carrying this round's tool calls
|
|
45
|
+
# @param tool_results [Array<Protege::ToolResult>] the paired results
|
|
46
|
+
# @return [void]
|
|
47
|
+
def persist_tool_round(turn:, response:, tool_results:)
|
|
48
|
+
Protege::ToolUse.record_round(
|
|
49
|
+
source: message,
|
|
50
|
+
persona:,
|
|
51
|
+
run_id: Protege::Current.correlation_id,
|
|
52
|
+
turn_index: turn,
|
|
53
|
+
tool_calls: response.tool_calls,
|
|
54
|
+
tool_results:
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Stream only for console messages — SMTP mail has no connected client. Matched against the
|
|
59
|
+
# configured console address case-insensitively, since email addresses are not case-sensitive in
|
|
60
|
+
# practice and the stored +from_address+ preserves whatever case arrived.
|
|
61
|
+
#
|
|
62
|
+
# @return [Boolean] true when the message came from the console address
|
|
63
|
+
def streaming?
|
|
64
|
+
message&.from_address.to_s.casecmp?(Protege.configuration.console_address.to_s)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
module Orchestrator
|
|
5
|
+
# Ordered list of resolver classes that build inference context for one persona. Configured at
|
|
6
|
+
# persona declaration time (via +Persona::Config#resolvers+) and run once per
|
|
7
|
+
# +Harness#build_initial_request+ call.
|
|
8
|
+
#
|
|
9
|
+
# Each resolver runs independently and returns +Array<ModelMessage>+ or +nil+. The chain
|
|
10
|
+
# concatenates all contributions in order — there is no wrapping or short-circuit (unlike
|
|
11
|
+
# middleware).
|
|
12
|
+
#
|
|
13
|
+
# Entries are stored as +[klass, args, kwargs, block]+ tuples rather than instances so that
|
|
14
|
+
# +remove(Klass)+ can locate and remove a specific resolver class without needing to match on
|
|
15
|
+
# constructor arguments. Each +run+ call creates fresh instances. A block passed to +use+/+prepend+
|
|
16
|
+
# is captured and forwarded to +klass.new+, so resolvers can take a loader block (e.g.
|
|
17
|
+
# +chain.use(Protege::LoadRecordResolver) { |ctx| Invoice.where(...) }+).
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# chain = ResolverChain.new
|
|
21
|
+
# chain.use Protege::LoadFileResolver, 'app/personas/agent/PERSONA.md'
|
|
22
|
+
# chain.use(Protege::LoadRecordResolver) { |ctx| Account.find_by(email: ctx.message.from_address) }
|
|
23
|
+
# chain.use Protege::ThreadHistoryResolver
|
|
24
|
+
#
|
|
25
|
+
# messages = chain.run(resolve_ctx) # => Array<ModelMessage>
|
|
26
|
+
class ResolverChain
|
|
27
|
+
# @return [Array<Array>] read-only view of the registered +[klass, args, kwargs, block]+ entries
|
|
28
|
+
attr_reader :entries
|
|
29
|
+
|
|
30
|
+
# Initialize an empty chain.
|
|
31
|
+
#
|
|
32
|
+
# @return [void]
|
|
33
|
+
def initialize
|
|
34
|
+
@entries = []
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Append a resolver class to the end of the chain. Constructor args are stored alongside and
|
|
38
|
+
# forwarded to +klass.new+ at run time.
|
|
39
|
+
#
|
|
40
|
+
# @param klass [Class] the resolver class to register
|
|
41
|
+
# @param args [Array] positional constructor arguments forwarded at run time
|
|
42
|
+
# @param kwargs [Hash] keyword constructor arguments forwarded at run time
|
|
43
|
+
# @yield optional block forwarded to +klass.new+ (e.g. a loader for +LoadRecordResolver+)
|
|
44
|
+
# @return [self] the chain, for fluent chaining
|
|
45
|
+
def use(klass, *args, **kwargs, &block)
|
|
46
|
+
@entries << [klass, args, kwargs, block]
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Insert a resolver class at the front of the chain.
|
|
51
|
+
#
|
|
52
|
+
# @param klass [Class] the resolver class to register
|
|
53
|
+
# @param args [Array] positional constructor arguments forwarded at run time
|
|
54
|
+
# @param kwargs [Hash] keyword constructor arguments forwarded at run time
|
|
55
|
+
# @yield optional block forwarded to +klass.new+ (e.g. a loader for +LoadRecordResolver+)
|
|
56
|
+
# @return [self] the chain, for fluent chaining
|
|
57
|
+
def prepend(klass, *args, **kwargs, &block)
|
|
58
|
+
@entries.unshift([klass, args, kwargs, block])
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Remove every entry whose registered class equals +klass+.
|
|
63
|
+
#
|
|
64
|
+
# @param klass [Class] the resolver class to remove
|
|
65
|
+
# @return [self] the chain, for fluent chaining
|
|
66
|
+
def remove(klass)
|
|
67
|
+
@entries.reject! { |entry| entry.first == klass }
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Empty the chain.
|
|
72
|
+
#
|
|
73
|
+
# @return [self] the now-empty chain
|
|
74
|
+
def clear
|
|
75
|
+
@entries.clear
|
|
76
|
+
self
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Run every resolver in order, passing +context+ to each.
|
|
80
|
+
#
|
|
81
|
+
# +nil+ returns are silently skipped (coerced through +Array(...)+). Fresh resolver instances
|
|
82
|
+
# are constructed per call from the stored +[klass, args, kwargs, block]+ tuples.
|
|
83
|
+
#
|
|
84
|
+
# @param context [Protege::Orchestrator::Context] the per-delivery resolver context
|
|
85
|
+
# @return [Array<Object>] a flat array of +ModelMessage+ objects, empty when none contribute
|
|
86
|
+
def run(context)
|
|
87
|
+
@entries.flat_map do |klass, args, kwargs, block|
|
|
88
|
+
Array(klass.new(*args, **kwargs, &block).resolve(context:))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
module Orchestrator
|
|
5
|
+
# Context for a proactive run — one driven by a scheduled +Responsibility+ (the Loop layer). Adds
|
|
6
|
+
# the +responsibility+ to the shared +Context+ surface so tools and resolvers can reach the duty
|
|
7
|
+
# being carried out (its prompt, schedule, owning persona). The +#message+ accessor inherited from
|
|
8
|
+
# +Context+ stays nil, so reply-only tools (e.g. +send_email+ in reply mode) degrade gracefully.
|
|
9
|
+
class ResponsibilityContext < Context
|
|
10
|
+
# @return [Protege::Responsibility] the responsibility being fulfilled
|
|
11
|
+
attr_reader :responsibility
|
|
12
|
+
|
|
13
|
+
# @param persona [Protege::Persona] the persona carrying out the responsibility
|
|
14
|
+
# @param responsibility [Protege::Responsibility] the scheduled duty being run
|
|
15
|
+
# @return [void]
|
|
16
|
+
def initialize(persona:, responsibility:)
|
|
17
|
+
@responsibility = responsibility
|
|
18
|
+
super(persona:)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
module Orchestrator
|
|
5
|
+
# The proactive run — carrying out one scheduled +Responsibility+ (the Loop layer). Runs the
|
|
6
|
+
# persona's +responsibility_resolvers+ chain, which assembles the *entire* message list — including
|
|
7
|
+
# the responsibility's instructions (e.g. via +LoadTextResolver(role: :user) { ctx.responsibility.instructions }+).
|
|
8
|
+
# The harness injects no user turn of its own (it inherits the base +user_content+ default of nil):
|
|
9
|
+
# context assembly is the resolver chain's job. There is no inbound message and no streaming. The
|
|
10
|
+
# +Harness+ base owns the tool loop; this subclass only supplies the responsibility-specific hooks.
|
|
11
|
+
class ResponsibilityHarness < Harness
|
|
12
|
+
# @param persona [Protege::Persona] the persona carrying out the responsibility
|
|
13
|
+
# @param responsibility [Protege::Responsibility] the scheduled duty to run
|
|
14
|
+
# @param responsibility_run [Protege::ResponsibilityRun] the run record this execution belongs to
|
|
15
|
+
# @return [void]
|
|
16
|
+
def initialize(persona:, responsibility:, responsibility_run:)
|
|
17
|
+
super(persona:)
|
|
18
|
+
@responsibility = responsibility
|
|
19
|
+
@responsibility_run = responsibility_run
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# @return [Protege::Responsibility] the responsibility being fulfilled
|
|
25
|
+
attr_reader :responsibility
|
|
26
|
+
|
|
27
|
+
# @return [Protege::ResponsibilityRun] the run record this execution belongs to
|
|
28
|
+
attr_reader :responsibility_run
|
|
29
|
+
|
|
30
|
+
# @return [Orchestrator::ResolverChain] the persona's scheduled-run chain
|
|
31
|
+
def resolvers
|
|
32
|
+
persona.responsibility_resolvers
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [Orchestrator::ResponsibilityContext] the shared per-run context carrying the responsibility
|
|
36
|
+
def context
|
|
37
|
+
@context ||= ResponsibilityContext.new(persona:, responsibility:)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Persist this tool-calling round as +ToolUse+ rows owned by the +ResponsibilityRun+ — an audit
|
|
41
|
+
# trail of what the scheduled run did, beside the run's own status. Not replayed (a scheduled run
|
|
42
|
+
# has no thread history); grouped by +correlation_id+ (this run) and +turn+.
|
|
43
|
+
#
|
|
44
|
+
# @param turn [Integer] the tool-calling round index within this run
|
|
45
|
+
# @param response [Object] the assistant response carrying this round's tool calls
|
|
46
|
+
# @param tool_results [Array<Protege::ToolResult>] the paired results
|
|
47
|
+
# @return [void]
|
|
48
|
+
def persist_tool_round(turn:, response:, tool_results:)
|
|
49
|
+
Protege::ToolUse.record_round(
|
|
50
|
+
source: responsibility_run,
|
|
51
|
+
persona:,
|
|
52
|
+
run_id: Protege::Current.correlation_id,
|
|
53
|
+
turn_index: turn,
|
|
54
|
+
tool_calls: response.tool_calls,
|
|
55
|
+
tool_results:
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# LOGI "O" — the orchestrator layer. Owns the inference-running constructs that sit between the
|
|
5
|
+
# Gateway (which produces inbound messages and sends replies) and the Inference layer (which talks
|
|
6
|
+
# to providers).
|
|
7
|
+
#
|
|
8
|
+
# The +Harness+ is this layer's primary construct: given one inbound message, it builds a provider
|
|
9
|
+
# request, runs the multi-turn tool loop, and produces a final response. Supporting constructs in
|
|
10
|
+
# this namespace include the +Context+ value objects passed to extensions, the +Tool+ and
|
|
11
|
+
# +Resolver+ contracts, and the +ResolverChain+ that assembles inference context per persona.
|
|
12
|
+
#
|
|
13
|
+
# Module methods here are the cross-namespace entry points. Callers (jobs, the +Extension+ tool mixin)
|
|
14
|
+
# use these rather than reaching into +Harness+ subclasses or +ToolResult+/+Error+ directly.
|
|
15
|
+
module Orchestrator
|
|
16
|
+
extend self
|
|
17
|
+
|
|
18
|
+
# Run a reactive reply generation for an inbound message.
|
|
19
|
+
#
|
|
20
|
+
# @param persona [Protege::Persona] the receiving persona
|
|
21
|
+
# @param message [Protege::Message] the inbound message to answer
|
|
22
|
+
# @return [Object] the harness result
|
|
23
|
+
def reply(persona:, message:)
|
|
24
|
+
ReplyHarness.run(persona:, message:)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Run a proactive generation for a scheduled responsibility.
|
|
28
|
+
#
|
|
29
|
+
# @param persona [Protege::Persona] the persona carrying out the duty
|
|
30
|
+
# @param responsibility [Protege::Responsibility] the scheduled duty
|
|
31
|
+
# @param responsibility_run [Protege::ResponsibilityRun] the run record being executed
|
|
32
|
+
# @return [Object] the harness result
|
|
33
|
+
def run_responsibility(persona:, responsibility:, responsibility_run:)
|
|
34
|
+
ResponsibilityHarness.run(persona:, responsibility:, responsibility_run:)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|