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
|
@@ -18,12 +18,12 @@ module Protege
|
|
|
18
18
|
#
|
|
19
19
|
# +Protege::Current.correlation_id+ must be set before calling so that emitted events carry the id
|
|
20
20
|
# automatically. Collaborates with +Inference+ (request building and generation), +Tool+ (dispatch
|
|
21
|
-
# and registry), the
|
|
21
|
+
# and registry), the agent's +ResolverChain+ (context assembly), and the +Protege::*Event+ classes
|
|
22
22
|
# (observability).
|
|
23
23
|
#
|
|
24
24
|
# @example
|
|
25
|
-
# ReplyHarness.run(
|
|
26
|
-
# ResponsibilityHarness.run(
|
|
25
|
+
# ReplyHarness.run(agent:, message:)
|
|
26
|
+
# ResponsibilityHarness.run(agent:, responsibility:)
|
|
27
27
|
#
|
|
28
28
|
# == Lifecycle
|
|
29
29
|
#
|
|
@@ -45,10 +45,10 @@ module Protege
|
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
# @param
|
|
48
|
+
# @param agent [Protege::Agent] the agent handling the run
|
|
49
49
|
# @return [void]
|
|
50
|
-
def initialize(
|
|
51
|
-
@
|
|
50
|
+
def initialize(agent:)
|
|
51
|
+
@agent = agent
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# Entry point. Emit lifecycle events around the tool loop.
|
|
@@ -61,22 +61,22 @@ module Protege
|
|
|
61
61
|
# @return [Object] the final +Provider::Response+
|
|
62
62
|
# @raise [StandardError] re-raised after +inference_failed+ is emitted
|
|
63
63
|
def run
|
|
64
|
-
InferenceStartedEvent.emit(
|
|
64
|
+
InferenceStartedEvent.emit(agent:, message:)
|
|
65
65
|
|
|
66
66
|
result = run_tool_loop
|
|
67
67
|
|
|
68
|
-
InferenceCompletedEvent.emit(
|
|
68
|
+
InferenceCompletedEvent.emit(agent:, message:, result:)
|
|
69
69
|
|
|
70
70
|
result
|
|
71
71
|
rescue StandardError => e
|
|
72
|
-
InferenceFailedEvent.emit(
|
|
72
|
+
InferenceFailedEvent.emit(agent:, message:, error: e)
|
|
73
73
|
raise
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
private
|
|
77
77
|
|
|
78
|
-
# @return [Protege::
|
|
79
|
-
attr_reader :
|
|
78
|
+
# @return [Protege::Agent] the agent handling the run
|
|
79
|
+
attr_reader :agent
|
|
80
80
|
|
|
81
81
|
# ── Kind-specific hooks (subclasses override) ─────────────────────────────
|
|
82
82
|
|
|
@@ -134,11 +134,11 @@ module Protege
|
|
|
134
134
|
return final if final.tool_calls.blank?
|
|
135
135
|
|
|
136
136
|
if turn >= (max_loop_iterations - 1)
|
|
137
|
-
InferenceMaxTurnsReachedEvent.emit(
|
|
137
|
+
InferenceMaxTurnsReachedEvent.emit(agent:, turn:, tool_calls: final.tool_calls)
|
|
138
138
|
break
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
-
ToolCallsReceivedEvent.emit(
|
|
141
|
+
ToolCallsReceivedEvent.emit(agent:, tool_calls: final.tool_calls)
|
|
142
142
|
|
|
143
143
|
tool_results = final.tool_calls.map { invoke_tool(_1) }
|
|
144
144
|
record_tool_round(turn:, response: final, tool_results:)
|
|
@@ -175,7 +175,7 @@ module Protege
|
|
|
175
175
|
# Emit the per-turn trace event: the wire request (+Request#to_wire+ — the very serialization the
|
|
176
176
|
# API call uses, so the snapshot is exact), the response, and the inference settings that shaped it
|
|
177
177
|
# (+model+ + +settings+, pulled from the provider the harness resolves — not global config — so a
|
|
178
|
-
# future
|
|
178
|
+
# future agent-level override is captured). Best-effort: any failure is logged and swallowed, so
|
|
179
179
|
# building or publishing the event can never abort a run whose side effects may already have run.
|
|
180
180
|
#
|
|
181
181
|
# @param turn [Integer] this generation's turn index
|
|
@@ -205,7 +205,7 @@ module Protege
|
|
|
205
205
|
def run_generation(request)
|
|
206
206
|
if streaming?
|
|
207
207
|
Inference.generate_stream(request) do |chunk|
|
|
208
|
-
InferenceChunkEvent.emit(
|
|
208
|
+
InferenceChunkEvent.emit(agent:, thread: message.email_thread, chunk:)
|
|
209
209
|
end
|
|
210
210
|
else
|
|
211
211
|
Inference.generate(request)
|
|
@@ -254,14 +254,14 @@ module Protege
|
|
|
254
254
|
# @param tool_call [Object] the tool call to dispatch
|
|
255
255
|
# @return [Protege::ToolResult] the call paired with its outcome
|
|
256
256
|
def invoke_tool(tool_call)
|
|
257
|
-
ToolCallStartedEvent.emit(
|
|
257
|
+
ToolCallStartedEvent.emit(agent:, tool_call:)
|
|
258
258
|
|
|
259
259
|
tool_result = ToolMixin.invoke(tool_call, context:)
|
|
260
260
|
|
|
261
261
|
if tool_result.result.success?
|
|
262
|
-
ToolCallCompletedEvent.emit(
|
|
262
|
+
ToolCallCompletedEvent.emit(agent:, tool_call:, result: tool_result.result)
|
|
263
263
|
else
|
|
264
|
-
ToolCallFailedEvent.emit(
|
|
264
|
+
ToolCallFailedEvent.emit(agent:, tool_call:, error: tool_result.result.error)
|
|
265
265
|
end
|
|
266
266
|
|
|
267
267
|
tool_result
|
|
@@ -284,19 +284,21 @@ module Protege
|
|
|
284
284
|
end
|
|
285
285
|
end
|
|
286
286
|
|
|
287
|
-
# Assemble the first +Provider::Request+ from the opening user turn, the
|
|
287
|
+
# Assemble the first +Provider::Request+ from the opening user turn, the agent's effective
|
|
288
288
|
# (scoped) tool set, inference config, and any context messages contributed by resolvers.
|
|
289
289
|
#
|
|
290
|
-
# The catalogue is +
|
|
291
|
-
#
|
|
292
|
-
#
|
|
293
|
-
#
|
|
290
|
+
# The catalogue is +agent.available_tools_for(context:)+ rather than the whole registry, so the
|
|
291
|
+
# agent's attached toolkits — and each attachment's sender gate — bound what the model is offered,
|
|
292
|
+
# on both the reply and the scheduled path, since both inherit this base method (the context's
|
|
293
|
+
# sender, or its absence on a scheduled run, drives toolkit gating). Later turns re-use
|
|
294
|
+
# +previous.tools+ (see +Inference.extend_request+), and a reply run has one fixed sender, so the
|
|
295
|
+
# scope holds for the run.
|
|
294
296
|
#
|
|
295
297
|
# @return [Object] the initial provider request
|
|
296
298
|
def build_initial_request
|
|
297
299
|
Inference.build_request(
|
|
298
300
|
user_content:,
|
|
299
|
-
tools:
|
|
301
|
+
tools: agent.available_tools_for(context:),
|
|
300
302
|
context_messages: resolvers.run(context)
|
|
301
303
|
)
|
|
302
304
|
end
|
|
@@ -9,12 +9,12 @@ module Protege
|
|
|
9
9
|
# @return [Object] the inbound message being processed
|
|
10
10
|
attr_reader :message
|
|
11
11
|
|
|
12
|
-
# @param
|
|
12
|
+
# @param agent [Protege::Agent] the agent handling the run
|
|
13
13
|
# @param message [Object] the inbound message being processed
|
|
14
14
|
# @return [void]
|
|
15
|
-
def initialize(
|
|
15
|
+
def initialize(agent:, message:)
|
|
16
16
|
@message = message
|
|
17
|
-
super(
|
|
17
|
+
super(agent:)
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
end
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module Protege
|
|
4
4
|
module Orchestrator
|
|
5
|
-
# The reactive run — answering one inbound email. Runs the receiving
|
|
5
|
+
# The reactive run — answering one inbound email. Runs the receiving agent's +message_resolvers+
|
|
6
6
|
# chain, opens the turn with the message body (plus any viewable attachments as vision parts), and
|
|
7
7
|
# streams tokens when the message came from the console. The +Harness+ base owns the tool loop;
|
|
8
8
|
# this subclass only supplies the reply-specific hooks.
|
|
9
9
|
class ReplyHarness < Harness
|
|
10
|
-
# @param
|
|
10
|
+
# @param agent [Protege::Agent] the agent that received the message
|
|
11
11
|
# @param message [Object] the inbound message to respond to
|
|
12
12
|
# @return [void]
|
|
13
|
-
def initialize(
|
|
14
|
-
super(
|
|
13
|
+
def initialize(agent:, message:)
|
|
14
|
+
super(agent:)
|
|
15
15
|
@message = message
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -20,9 +20,9 @@ module Protege
|
|
|
20
20
|
# @return [Object] the inbound message being answered (overrides the base nil)
|
|
21
21
|
attr_reader :message
|
|
22
22
|
|
|
23
|
-
# @return [Orchestrator::ResolverChain] the
|
|
23
|
+
# @return [Orchestrator::ResolverChain] the agent's reply chain
|
|
24
24
|
def resolvers
|
|
25
|
-
|
|
25
|
+
agent.message_resolvers
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# @return [String, Array<Object>] the message body, or body-plus-attachment parts
|
|
@@ -32,7 +32,7 @@ module Protege
|
|
|
32
32
|
|
|
33
33
|
# @return [Orchestrator::ReplyContext] the shared per-run context carrying the message
|
|
34
34
|
def context
|
|
35
|
-
@context ||= ReplyContext.new(
|
|
35
|
+
@context ||= ReplyContext.new(agent:, message:)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# Persist this tool-calling round as +ToolUse+ rows owned by the inbound message, so a later turn's
|
|
@@ -47,7 +47,7 @@ module Protege
|
|
|
47
47
|
def persist_tool_round(turn:, response:, tool_results:)
|
|
48
48
|
Protege::ToolUse.record_round(
|
|
49
49
|
source: message,
|
|
50
|
-
|
|
50
|
+
agent:,
|
|
51
51
|
run_id: Protege::Current.correlation_id,
|
|
52
52
|
turn_index: turn,
|
|
53
53
|
tool_calls: response.tool_calls,
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Protege
|
|
4
4
|
module Orchestrator
|
|
5
|
-
# Ordered list of resolver classes that build inference context for one
|
|
6
|
-
#
|
|
5
|
+
# Ordered list of resolver classes that build inference context for one agent. Configured at
|
|
6
|
+
# agent declaration time (via +Agent::Config#resolvers+) and run once per
|
|
7
7
|
# +Harness#build_initial_request+ call.
|
|
8
8
|
#
|
|
9
9
|
# Each resolver runs independently and returns +Array<ModelMessage>+ or +nil+. The chain
|
|
@@ -18,7 +18,7 @@ module Protege
|
|
|
18
18
|
#
|
|
19
19
|
# @example
|
|
20
20
|
# chain = ResolverChain.new
|
|
21
|
-
# chain.use Protege::LoadFileResolver, 'app/
|
|
21
|
+
# chain.use Protege::LoadFileResolver, 'app/agents/executive/AGENT.md'
|
|
22
22
|
# chain.use(Protege::LoadRecordResolver) { |ctx| Account.find_by(email: ctx.message.from_address) }
|
|
23
23
|
# chain.use Protege::ThreadHistoryResolver
|
|
24
24
|
#
|
|
@@ -4,18 +4,18 @@ module Protege
|
|
|
4
4
|
module Orchestrator
|
|
5
5
|
# Context for a proactive run — one driven by a scheduled +Responsibility+ (the Loop layer). Adds
|
|
6
6
|
# the +responsibility+ to the shared +Context+ surface so tools and resolvers can reach the duty
|
|
7
|
-
# being carried out (its prompt, schedule, owning
|
|
7
|
+
# being carried out (its prompt, schedule, owning agent). The +#message+ accessor inherited from
|
|
8
8
|
# +Context+ stays nil, so reply-only tools (e.g. +send_email+ in reply mode) degrade gracefully.
|
|
9
9
|
class ResponsibilityContext < Context
|
|
10
10
|
# @return [Protege::Responsibility] the responsibility being fulfilled
|
|
11
11
|
attr_reader :responsibility
|
|
12
12
|
|
|
13
|
-
# @param
|
|
13
|
+
# @param agent [Protege::Agent] the agent carrying out the responsibility
|
|
14
14
|
# @param responsibility [Protege::Responsibility] the scheduled duty being run
|
|
15
15
|
# @return [void]
|
|
16
|
-
def initialize(
|
|
16
|
+
def initialize(agent:, responsibility:)
|
|
17
17
|
@responsibility = responsibility
|
|
18
|
-
super(
|
|
18
|
+
super(agent:)
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
end
|
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
module Protege
|
|
4
4
|
module Orchestrator
|
|
5
5
|
# The proactive run — carrying out one scheduled +Responsibility+ (the Loop layer). Runs the
|
|
6
|
-
#
|
|
6
|
+
# agent's +responsibility_resolvers+ chain, which assembles the *entire* message list — including
|
|
7
7
|
# the responsibility's instructions (e.g. via +LoadTextResolver(role: :user) { ctx.responsibility.instructions }+).
|
|
8
8
|
# The harness injects no user turn of its own (it inherits the base +user_content+ default of nil):
|
|
9
9
|
# context assembly is the resolver chain's job. There is no inbound message and no streaming. The
|
|
10
10
|
# +Harness+ base owns the tool loop; this subclass only supplies the responsibility-specific hooks.
|
|
11
11
|
class ResponsibilityHarness < Harness
|
|
12
|
-
# @param
|
|
12
|
+
# @param agent [Protege::Agent] the agent carrying out the responsibility
|
|
13
13
|
# @param responsibility [Protege::Responsibility] the scheduled duty to run
|
|
14
14
|
# @param responsibility_run [Protege::ResponsibilityRun] the run record this execution belongs to
|
|
15
15
|
# @return [void]
|
|
16
|
-
def initialize(
|
|
17
|
-
super(
|
|
16
|
+
def initialize(agent:, responsibility:, responsibility_run:)
|
|
17
|
+
super(agent:)
|
|
18
18
|
@responsibility = responsibility
|
|
19
19
|
@responsibility_run = responsibility_run
|
|
20
20
|
end
|
|
@@ -27,14 +27,14 @@ module Protege
|
|
|
27
27
|
# @return [Protege::ResponsibilityRun] the run record this execution belongs to
|
|
28
28
|
attr_reader :responsibility_run
|
|
29
29
|
|
|
30
|
-
# @return [Orchestrator::ResolverChain] the
|
|
30
|
+
# @return [Orchestrator::ResolverChain] the agent's scheduled-run chain
|
|
31
31
|
def resolvers
|
|
32
|
-
|
|
32
|
+
agent.responsibility_resolvers
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# @return [Orchestrator::ResponsibilityContext] the shared per-run context carrying the responsibility
|
|
36
36
|
def context
|
|
37
|
-
@context ||= ResponsibilityContext.new(
|
|
37
|
+
@context ||= ResponsibilityContext.new(agent:, responsibility:)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# Persist this tool-calling round as +ToolUse+ rows owned by the +ResponsibilityRun+ — an audit
|
|
@@ -48,7 +48,7 @@ module Protege
|
|
|
48
48
|
def persist_tool_round(turn:, response:, tool_results:)
|
|
49
49
|
Protege::ToolUse.record_round(
|
|
50
50
|
source: responsibility_run,
|
|
51
|
-
|
|
51
|
+
agent:,
|
|
52
52
|
run_id: Protege::Current.correlation_id,
|
|
53
53
|
turn_index: turn,
|
|
54
54
|
tool_calls: response.tool_calls,
|
data/lib/protege/orchestrator.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Protege
|
|
|
8
8
|
# The +Harness+ is this layer's primary construct: given one inbound message, it builds a provider
|
|
9
9
|
# request, runs the multi-turn tool loop, and produces a final response. Supporting constructs in
|
|
10
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
|
|
11
|
+
# +Resolver+ contracts, and the +ResolverChain+ that assembles inference context per agent.
|
|
12
12
|
#
|
|
13
13
|
# Module methods here are the cross-namespace entry points. Callers (jobs, the +Extension+ tool mixin)
|
|
14
14
|
# use these rather than reaching into +Harness+ subclasses or +ToolResult+/+Error+ directly.
|
|
@@ -17,21 +17,21 @@ module Protege
|
|
|
17
17
|
|
|
18
18
|
# Run a reactive reply generation for an inbound message.
|
|
19
19
|
#
|
|
20
|
-
# @param
|
|
20
|
+
# @param agent [Protege::Agent] the receiving agent
|
|
21
21
|
# @param message [Protege::Message] the inbound message to answer
|
|
22
22
|
# @return [Object] the harness result
|
|
23
|
-
def reply(
|
|
24
|
-
ReplyHarness.run(
|
|
23
|
+
def reply(agent:, message:)
|
|
24
|
+
ReplyHarness.run(agent:, message:)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# Run a proactive generation for a scheduled responsibility.
|
|
28
28
|
#
|
|
29
|
-
# @param
|
|
29
|
+
# @param agent [Protege::Agent] the agent carrying out the duty
|
|
30
30
|
# @param responsibility [Protege::Responsibility] the scheduled duty
|
|
31
31
|
# @param responsibility_run [Protege::ResponsibilityRun] the run record being executed
|
|
32
32
|
# @return [Object] the harness result
|
|
33
|
-
def run_responsibility(
|
|
34
|
-
ResponsibilityHarness.run(
|
|
33
|
+
def run_responsibility(agent:, responsibility:, responsibility_run:)
|
|
34
|
+
ResponsibilityHarness.run(agent:, responsibility:, responsibility_run:)
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
module Subscribers
|
|
5
|
+
# The operational-alert subscriber — emails the platform admin when an inference run fails. A peer of
|
|
6
|
+
# +Subscribers::Tracing+ / +HookDispatcher+ / +IntrospectionBroadcaster+, installed once at boot.
|
|
7
|
+
# Subscribes to +InferenceFailedEvent+ and, when a host has opted in via +config.failure_alerts+,
|
|
8
|
+
# sends a +Protege::AlertMailer+ notice carrying the error and the run's correlation id.
|
|
9
|
+
#
|
|
10
|
+
# Opt-in and best-effort by design:
|
|
11
|
+
#
|
|
12
|
+
# * Off until +config.failure_alerts[:to]+ is set, and it will not send without a +:from+ — there is
|
|
13
|
+
# no deliverable default sender (a real +:from+ must be a DKIM-signable address at a registered
|
|
14
|
+
# +EmailDomain+), so a +:to+ with no +:from+ is a misconfiguration that logs and sends nothing.
|
|
15
|
+
# * +InferenceFailedEvent+ fires once per failed *run*, and the job layer retries a transient failure
|
|
16
|
+
# up to three times — so a single failing message emits the event several times, all sharing one
|
|
17
|
+
# correlation id. Delivery is deduped on that id so the retry storm collapses to one alert, and a
|
|
18
|
+
# global per-window cap guards against a systemic outage flooding the admin.
|
|
19
|
+
# * The event is emitted from inside the Harness's rescue, right before it re-raises, and
|
|
20
|
+
# +ActiveSupport::Notifications+ propagates subscriber exceptions — so this module rescues
|
|
21
|
+
# everything internally. An alert failure must never mask the inference error that triggered it.
|
|
22
|
+
module Alerter
|
|
23
|
+
extend self
|
|
24
|
+
|
|
25
|
+
# How long a run's correlation id suppresses duplicate alerts (covers the retry backoff window).
|
|
26
|
+
DEDUP_TTL = 1.hour
|
|
27
|
+
|
|
28
|
+
# Cache key + ceiling + window for the systemic-outage flood guard.
|
|
29
|
+
GLOBAL_KEY = 'protege:alert:global'
|
|
30
|
+
GLOBAL_CAP = 10
|
|
31
|
+
GLOBAL_TTL = 1.hour
|
|
32
|
+
|
|
33
|
+
# Live subscription handles, retained so {reset!} can detach them.
|
|
34
|
+
@handles = []
|
|
35
|
+
|
|
36
|
+
# Subscribe the alerter to inference failures — the subscriber's +install!+ contract. Idempotent;
|
|
37
|
+
# called once at boot from the engine's +after_initialize+.
|
|
38
|
+
#
|
|
39
|
+
# @return [void]
|
|
40
|
+
def install!
|
|
41
|
+
return unless @handles.empty?
|
|
42
|
+
|
|
43
|
+
@handles = [InferenceFailedEvent.subscribe { |event| alert(event) }]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Detach every subscription — the subscriber's +reset!+ contract, for boot re-wiring and tests.
|
|
47
|
+
#
|
|
48
|
+
# @return [void]
|
|
49
|
+
def reset!
|
|
50
|
+
@handles.each { |handle| ActiveSupport::Notifications.unsubscribe(handle) }
|
|
51
|
+
@handles = []
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Email the configured recipients about one failed run. Best-effort: gated on opt-in config,
|
|
57
|
+
# deduped, capped, and wrapped so any failure is logged and swallowed.
|
|
58
|
+
#
|
|
59
|
+
# @param event [Protege::InferenceFailedEvent] the failure event
|
|
60
|
+
# @return [void]
|
|
61
|
+
def alert(event)
|
|
62
|
+
return unless enabled?
|
|
63
|
+
return unless first_failure?(event)
|
|
64
|
+
return unless under_global_cap?
|
|
65
|
+
|
|
66
|
+
Protege::AlertMailer.inference_failed(
|
|
67
|
+
error_class: event.error.class.name,
|
|
68
|
+
error_message: event.error.message,
|
|
69
|
+
correlation_id: event.correlation_id
|
|
70
|
+
).deliver_now
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
logger.error("[Protege] alert delivery failed: #{e.class}: #{e.message}")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Whether alerting is configured to send. Requires recipients and a sender: without a deliverable
|
|
76
|
+
# +:from+ there is nothing safe to send from, so a +:to+ alone is a logged misconfiguration.
|
|
77
|
+
#
|
|
78
|
+
# @return [Boolean]
|
|
79
|
+
def enabled?
|
|
80
|
+
alerts = Protege.configuration.failure_alerts
|
|
81
|
+
return false if Array(alerts[:to]).reject(&:blank?).empty?
|
|
82
|
+
|
|
83
|
+
if alerts[:from].blank?
|
|
84
|
+
logger.warn('[Protege] failure_alerts :to is set but :from is missing; not sending ' \
|
|
85
|
+
'(a deliverable :from at a registered EmailDomain is required)')
|
|
86
|
+
return false
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Claim the first alert for this run's correlation id, so the job's retry storm sends only one.
|
|
93
|
+
# Returns true only for the first failure seen within {DEDUP_TTL}.
|
|
94
|
+
#
|
|
95
|
+
# @param event [Protege::InferenceFailedEvent] the failure event
|
|
96
|
+
# @return [Boolean]
|
|
97
|
+
def first_failure?(event)
|
|
98
|
+
Rails.cache.write("protege:alert:#{event.correlation_id}", true, expires_in: DEDUP_TTL, unless_exist: true)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Whether this alert is within the global per-window cap. A best-effort read-modify-write (not
|
|
102
|
+
# atomic) — adequate as a flood guard, and store-agnostic (no reliance on +increment+ semantics).
|
|
103
|
+
#
|
|
104
|
+
# @return [Boolean]
|
|
105
|
+
def under_global_cap?
|
|
106
|
+
count = (Rails.cache.read(GLOBAL_KEY) || 0) + 1
|
|
107
|
+
Rails.cache.write(GLOBAL_KEY, count, expires_in: GLOBAL_TTL)
|
|
108
|
+
count <= GLOBAL_CAP
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @return [Logger] the engine logger
|
|
112
|
+
def logger
|
|
113
|
+
Protege.configuration.logger
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -14,7 +14,7 @@ module Protege
|
|
|
14
14
|
#
|
|
15
15
|
# A trace records only what affects training: the wire +request+ and +response+ (each already
|
|
16
16
|
# serialized by the value object's own +#to_wire+, the same bytes the API exchange uses) and the
|
|
17
|
-
# inference settings (+model+ + +settings+). No
|
|
17
|
+
# inference settings (+model+ + +settings+). No agent/provider identity is stored, and this module
|
|
18
18
|
# does no serialization of its own — it just persists what the event carries.
|
|
19
19
|
module Tracing
|
|
20
20
|
extend self
|
data/lib/protege/version.rb
CHANGED
data/lib/protege.rb
CHANGED
|
@@ -9,12 +9,12 @@ require 'protege/engine'
|
|
|
9
9
|
# Rails application into an agentic workspace.
|
|
10
10
|
#
|
|
11
11
|
# Protege is structured around the LOGI pipeline:
|
|
12
|
-
# - **L**oop — scheduled responsibilities that drive
|
|
12
|
+
# - **L**oop — scheduled responsibilities that drive agents on a cron tick.
|
|
13
13
|
# - **O**rchestrator — the +Harness+ multi-turn tool loop that runs inference and dispatches tools.
|
|
14
14
|
# - **G**ateway — Action Mailbox inbound delivery and SMTP outbound delivery.
|
|
15
15
|
# - **I**nference — provider adapters that talk to model APIs.
|
|
16
16
|
#
|
|
17
|
-
# Email is the channel, the host Rails app is the workspace, and
|
|
17
|
+
# Email is the channel, the host Rails app is the workspace, and agents, tools, providers,
|
|
18
18
|
# resolvers, and hooks all participate as extensions. Requiring this file loads only the engine
|
|
19
19
|
# entry points; runtime constructs are resolved through Rails autoloading once the engine boots.
|
|
20
20
|
#
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
namespace :protege do
|
|
4
|
+
namespace :toolkits do
|
|
5
|
+
desc 'Ensure the system toolkits (All Tools / Default Tools) exist and refresh All Tools to the ' \
|
|
6
|
+
'full tool registry. Idempotent — run on each deploy (e.g. after db:migrate).'
|
|
7
|
+
task sync: :environment do
|
|
8
|
+
# Rake tasks don't eager-load by default, so the tool manifest would be incomplete; force it so
|
|
9
|
+
# All Tools captures every registered tool.
|
|
10
|
+
Rails.application.eager_load!
|
|
11
|
+
Protege::SystemToolkits.sync!
|
|
12
|
+
puts "Synced system toolkits — All Tools has #{Protege::Toolkit.all_tools&.member_tool_ids&.size} tools."
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
4
16
|
namespace :css do
|
|
5
17
|
desc "Build the Protege engine's compiled Tailwind CSS. " \
|
|
6
18
|
'Run this after changing engine views. Requires tailwindcss-rails in the host app.'
|