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
|
@@ -7,7 +7,7 @@ module Protege
|
|
|
7
7
|
# UI, the inbox, and the +ThreadHistoryResolver+ resolver all read from this model. The underlying
|
|
8
8
|
# +ActionMailbox::InboundEmail+ is retained only as a raw delivery-pipeline artifact; application
|
|
9
9
|
# logic never queries it directly. Every message belongs to exactly one +EmailThread+ and one
|
|
10
|
-
# +
|
|
10
|
+
# +Agent+, and inbound messages link back to the +InboundEmail+ they were ingested from.
|
|
11
11
|
#
|
|
12
12
|
# == Lifecycle (inbound)
|
|
13
13
|
#
|
|
@@ -15,7 +15,7 @@ module Protege
|
|
|
15
15
|
# processing → InferenceJob is running
|
|
16
16
|
# processed → InferenceJob completed successfully
|
|
17
17
|
# failed → InferenceJob raised; error captured in +last_processing_error+
|
|
18
|
-
# bounced → no matching
|
|
18
|
+
# bounced → no matching agent; handled in AgentMailbox before a job is queued
|
|
19
19
|
#
|
|
20
20
|
# == Lifecycle (outbound)
|
|
21
21
|
#
|
|
@@ -28,7 +28,7 @@ module Protege
|
|
|
28
28
|
|
|
29
29
|
# ── Associations ────────────────────────────────────────────────────────
|
|
30
30
|
|
|
31
|
-
belongs_to :
|
|
31
|
+
belongs_to :agent, class_name: 'Protege::Agent'
|
|
32
32
|
belongs_to :email_thread, class_name: 'Protege::EmailThread', inverse_of: :messages
|
|
33
33
|
# Optional: only inbound messages originate from an ActionMailbox record.
|
|
34
34
|
belongs_to :inbound_email, class_name: 'ActionMailbox::InboundEmail', optional: true
|
|
@@ -64,8 +64,9 @@ module Protege
|
|
|
64
64
|
|
|
65
65
|
# ── Validations ─────────────────────────────────────────────────────────
|
|
66
66
|
|
|
67
|
-
# Message-ID is the dedup key; uniqueness
|
|
68
|
-
|
|
67
|
+
# Message-ID is the dedup key; uniqueness is per agent so one email to several proteges records a
|
|
68
|
+
# message for each, while a single agent's retries stay idempotent.
|
|
69
|
+
validates :message_id, presence: true, uniqueness: { scope: :agent_id }
|
|
69
70
|
validates :direction, presence: true
|
|
70
71
|
validates :from_address, presence: true
|
|
71
72
|
validates :sent_at, presence: true
|
|
@@ -86,19 +87,19 @@ module Protege
|
|
|
86
87
|
# Ingest an +ActionMailbox::InboundEmail+ into a persisted, processing +Message+.
|
|
87
88
|
#
|
|
88
89
|
# Finds or creates the owning +EmailThread+, copies all mail-derived attributes onto the
|
|
89
|
-
# record, and transitions it to +processing+. Keyed on +inbound_email_id+, so
|
|
90
|
-
#
|
|
91
|
-
#
|
|
90
|
+
# record, and transitions it to +processing+. Keyed on +inbound_email_id+ *and* +agent+, so a
|
|
91
|
+
# retry for the same agent updates its existing record, while a different agent addressed by the
|
|
92
|
+
# same email gets its own — one inbound email maps to one message per handling agent.
|
|
92
93
|
#
|
|
93
94
|
# @param inbound_email [ActionMailbox::InboundEmail] the raw received email
|
|
94
|
-
# @param
|
|
95
|
+
# @param agent [Protege::Agent] the agent the email routed to
|
|
95
96
|
# @return [Protege::Message] the saved message in the +processing+ state
|
|
96
|
-
def ingest!(inbound_email:,
|
|
97
|
+
def ingest!(inbound_email:, agent:)
|
|
97
98
|
mail = inbound_email.mail
|
|
98
|
-
thread = EmailThread.find_or_create_for(mail:,
|
|
99
|
+
thread = EmailThread.find_or_create_for(mail:, agent:)
|
|
99
100
|
|
|
100
|
-
find_or_initialize_by(inbound_email_id: inbound_email.id).tap do |record|
|
|
101
|
-
record.assign_from_mail(mail:, thread:,
|
|
101
|
+
find_or_initialize_by(inbound_email_id: inbound_email.id, agent:).tap do |record|
|
|
102
|
+
record.assign_from_mail(mail:, thread:, agent:, inbound_email:)
|
|
102
103
|
record.start_processing!
|
|
103
104
|
end
|
|
104
105
|
end
|
|
@@ -111,14 +112,14 @@ module Protege
|
|
|
111
112
|
# @param mail [Mail::Message] the parsed email to copy attributes from
|
|
112
113
|
# @param direction [Symbol, String] +:inbound+ or +:outbound+
|
|
113
114
|
# @param thread [Protege::EmailThread] the owning thread
|
|
114
|
-
# @param
|
|
115
|
+
# @param agent [Protege::Agent] the agent the message belongs to
|
|
115
116
|
# @param inbound_email [ActionMailbox::InboundEmail, nil] the source record, if any
|
|
116
117
|
# @param attach_from_mail [Boolean] rebuild attachments from the mail's parts (true for inbound,
|
|
117
118
|
# whose files arrive as bytes); pass false for outbound, where the caller attaches known blobs.
|
|
118
119
|
# @return [Protege::Message] an unsaved message with attributes assigned
|
|
119
|
-
def build_from_mail(mail:, direction:, thread:,
|
|
120
|
+
def build_from_mail(mail:, direction:, thread:, agent:, inbound_email: nil, attach_from_mail: true)
|
|
120
121
|
new.tap do |record|
|
|
121
|
-
record.assign_from_mail(mail:, thread:,
|
|
122
|
+
record.assign_from_mail(mail:, thread:, agent:, inbound_email:, attach_from_mail:)
|
|
122
123
|
record.direction = direction
|
|
123
124
|
end
|
|
124
125
|
end
|
|
@@ -237,20 +238,20 @@ module Protege
|
|
|
237
238
|
|
|
238
239
|
# Copy every RFC 2822-derived attribute from a +Mail::Message+ onto this record.
|
|
239
240
|
#
|
|
240
|
-
# Shared by both {.ingest!} and {.build_from_mail}. Sets the thread/
|
|
241
|
+
# Shared by both {.ingest!} and {.build_from_mail}. Sets the thread/agent associations and
|
|
241
242
|
# then delegates the header, body, and threading field groups to private helpers so the work
|
|
242
243
|
# stays within Active Record's metrics budget. Does not save.
|
|
243
244
|
#
|
|
244
245
|
# @param mail [Mail::Message] the parsed email to read attributes from
|
|
245
246
|
# @param thread [Protege::EmailThread] the owning thread
|
|
246
|
-
# @param
|
|
247
|
+
# @param agent [Protege::Agent] the agent the message belongs to
|
|
247
248
|
# @param inbound_email [ActionMailbox::InboundEmail, nil] the source record, if any
|
|
248
249
|
# @param attach_from_mail [Boolean] rebuild attachments from the mail's MIME parts; true for inbound
|
|
249
250
|
# (files arrive as bytes), false for outbound (the caller attaches the known blobs by reference).
|
|
250
251
|
# @return [void]
|
|
251
|
-
def assign_from_mail(mail:, thread:,
|
|
252
|
+
def assign_from_mail(mail:, thread:, agent:, inbound_email: nil, attach_from_mail: true)
|
|
252
253
|
self.email_thread = thread
|
|
253
|
-
self.
|
|
254
|
+
self.agent = agent
|
|
254
255
|
self.inbound_email = inbound_email
|
|
255
256
|
|
|
256
257
|
assign_envelope_from(mail)
|
|
@@ -299,6 +300,21 @@ module Protege
|
|
|
299
300
|
)
|
|
300
301
|
end
|
|
301
302
|
|
|
303
|
+
# How many agent hops this message has already taken, read from the linked raw inbound email's
|
|
304
|
+
# +X-Protege-Recursion+ header. Zero for human mail (mail clients never echo the header back) and
|
|
305
|
+
# when the raw email is gone (Action Mailbox incinerates it eventually) — either way the chain
|
|
306
|
+
# resets, which is the intended behavior. A reply to this message stamps this count plus one (see
|
|
307
|
+
# +Gateway::RECURSION_HEADER+).
|
|
308
|
+
#
|
|
309
|
+
# @return [Integer] the inbound hop count, or 0 when untracked
|
|
310
|
+
def recursion_hops
|
|
311
|
+
raw = inbound_email&.mail
|
|
312
|
+
return 0 unless raw
|
|
313
|
+
|
|
314
|
+
field = raw.header[Gateway::RECURSION_HEADER]
|
|
315
|
+
field ? field.value.to_i : 0
|
|
316
|
+
end
|
|
317
|
+
|
|
302
318
|
private
|
|
303
319
|
|
|
304
320
|
# Assemble {#to_llm_text} from the standard header, the readable body (when present), and an optional
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Protege
|
|
4
|
-
#
|
|
4
|
+
# An agent's standing, cron-scheduled duty — the record behind the Loop layer (the "L" of LOGI).
|
|
5
5
|
#
|
|
6
6
|
# Where an inbound email drives the reactive path, a responsibility drives the *proactive* one: on
|
|
7
|
-
# its +schedule+ (a 5-field cron string) the Loop scheduler runs the owning
|
|
7
|
+
# its +schedule+ (a 5-field cron string) the Loop scheduler runs the owning agent's
|
|
8
8
|
# +responsibility_resolvers+ chain, seeded with this responsibility's +instructions+ as the opening user
|
|
9
|
-
# turn. The
|
|
9
|
+
# turn. The agent decides what to do from there (typically sending mail via +send_email+).
|
|
10
10
|
#
|
|
11
|
-
# Responsibilities are lean, dashboard-managed records — not a developer subclass; the per-
|
|
12
|
-
# resolver chain lives on the +
|
|
11
|
+
# Responsibilities are lean, dashboard-managed records — not a developer subclass; the per-agent
|
|
12
|
+
# resolver chain lives on the +Agent+, so a responsibility is just "which agent, what task, how
|
|
13
13
|
# often." Each execution is tracked by a +ResponsibilityRun+.
|
|
14
14
|
class Responsibility < ApplicationRecord
|
|
15
15
|
self.table_name = 'protege_responsibilities'
|
|
16
16
|
|
|
17
17
|
# ── Concerns ──────────────────────────────────────────────────────────────
|
|
18
18
|
|
|
19
|
-
# Keep the
|
|
19
|
+
# Keep the agent's Responsibilities sidebar live (append/replace/remove).
|
|
20
20
|
include BroadcastableResponsibility
|
|
21
21
|
|
|
22
22
|
# ── Associations ──────────────────────────────────────────────────────────
|
|
23
23
|
|
|
24
|
-
# The
|
|
24
|
+
# The agent that carries out this duty; its +responsibility_resolvers+ chain assembles context
|
|
25
25
|
# and its identity is the enforced +From+ on any mail the run sends.
|
|
26
|
-
belongs_to :
|
|
26
|
+
belongs_to :agent, class_name: 'Protege::Agent'
|
|
27
27
|
|
|
28
28
|
# Every execution of this responsibility, newest interesting for the dashboard; destroyed with it.
|
|
29
29
|
has_many :responsibility_runs,
|
|
@@ -48,14 +48,14 @@ module Protege
|
|
|
48
48
|
# Only responsibilities currently enabled to fire.
|
|
49
49
|
scope :active, -> { where(active: true) }
|
|
50
50
|
|
|
51
|
-
# Only responsibilities whose
|
|
51
|
+
# Only responsibilities whose agent is active (not archived) — archiving an agent stops its
|
|
52
52
|
# scheduled runs. The scheduler intersects this with {.active}.
|
|
53
|
-
scope :
|
|
53
|
+
scope :with_active_agent, -> { joins(:agent).merge(Protege::Agent.active) }
|
|
54
54
|
|
|
55
55
|
# ── Instance methods ────────────────────────────────────────────────────
|
|
56
56
|
|
|
57
57
|
# Whether this responsibility should fire at +time+ — active and its cron matches that minute.
|
|
58
|
-
# The scheduler calls this each tick (over the {.
|
|
58
|
+
# The scheduler calls this each tick (over the {.with_active_agent} set, so an archived agent's
|
|
59
59
|
# duties are already excluded); there is no precomputed next-run, only minute matching.
|
|
60
60
|
#
|
|
61
61
|
# @param time [Time] the moment to test (the tick's +Time.current+)
|
|
@@ -78,7 +78,7 @@ module Protege
|
|
|
78
78
|
run = responsibility_runs.create!(status: :pending, correlation_id: SecureRandom.uuid)
|
|
79
79
|
Protege::Current.correlation_id = run.correlation_id
|
|
80
80
|
|
|
81
|
-
LoopRunEnqueuedEvent.emit(name:,
|
|
81
|
+
LoopRunEnqueuedEvent.emit(name:, agent_name: agent.name)
|
|
82
82
|
ResponsibilityJob.perform_later(run_id: run.id)
|
|
83
83
|
run
|
|
84
84
|
end
|
|
@@ -13,7 +13,7 @@ module Protege
|
|
|
13
13
|
# result (the tool loop produces them 1:1). +run_id+ groups one harness run, +turn_index+ the
|
|
14
14
|
# tool-calling round within it, and +position+ the order within a round (an assistant turn may request
|
|
15
15
|
# several calls at once). +tool_call_id+ correlates the call to its result when history is replayed.
|
|
16
|
-
# +
|
|
16
|
+
# +agent_id+ is denormalized for per-agent queries.
|
|
17
17
|
class ToolUse < ApplicationRecord
|
|
18
18
|
# Upper bound on a stored +arguments+/+result+ JSON payload, so a huge tool result (e.g. a large
|
|
19
19
|
# +web_fetch+ body) can't bloat the row. Replay truncates further still (see +#replay_result+).
|
|
@@ -24,8 +24,8 @@ module Protege
|
|
|
24
24
|
|
|
25
25
|
# ── Associations ────────────────────────────────────────────────────────
|
|
26
26
|
|
|
27
|
-
belongs_to :source,
|
|
28
|
-
belongs_to :
|
|
27
|
+
belongs_to :source, polymorphic: true
|
|
28
|
+
belongs_to :agent, class_name: 'Protege::Agent'
|
|
29
29
|
|
|
30
30
|
# ── Validations ─────────────────────────────────────────────────────────
|
|
31
31
|
|
|
@@ -78,18 +78,18 @@ module Protege
|
|
|
78
78
|
# finds the existing rows instead of duplicating them.
|
|
79
79
|
#
|
|
80
80
|
# @param source [Protege::Message, Protege::ResponsibilityRun] the run's owning record
|
|
81
|
-
# @param
|
|
81
|
+
# @param agent [Protege::Agent] the agent handling the run
|
|
82
82
|
# @param run_id [String, nil] id grouping this harness run
|
|
83
83
|
# @param turn_index [Integer] the tool-calling round within the run (0-based)
|
|
84
84
|
# @param tool_calls [Array<Protege::Inference::Provider::ToolCall>] the calls the model requested
|
|
85
85
|
# @param tool_results [Array<Protege::ToolResult>] the paired results
|
|
86
86
|
# @return [Array<Protege::ToolUse>] the recorded rows
|
|
87
|
-
def record_round(source:,
|
|
87
|
+
def record_round(source:, agent:, run_id:, turn_index:, tool_calls:, tool_results:)
|
|
88
88
|
transaction do
|
|
89
89
|
tool_calls.each_with_index.map do |call, position|
|
|
90
90
|
result = tool_results[position]
|
|
91
91
|
find_or_create_by!(source:, run_id:, turn_index:, position:) do |use|
|
|
92
|
-
use.
|
|
92
|
+
use.agent = agent
|
|
93
93
|
use.tool_call_id = call.id
|
|
94
94
|
use.tool_name = call.name
|
|
95
95
|
use.arguments = clip(call.input.to_json, limit: STORED_PAYLOAD_LIMIT)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# A toolkit — the first "Binding" of the extension framework. A global, dashboard-managed, reusable
|
|
5
|
+
# bundle of tools (referenced by their manifest ids) and nothing more: it says *what* tools travel
|
|
6
|
+
# together, not *who* may use them. Permission is a property of the attachment — see +AgentToolkit+ —
|
|
7
|
+
# so the same toolkit given to two agents can be gated differently, with no tool-list duplication.
|
|
8
|
+
#
|
|
9
|
+
# Member tools are a +member_tool_ids+ JSON id list (a checklist over the tool manifest), reconciled
|
|
10
|
+
# against the live manifest at read time so a stale id (a tool since removed from the code) is skipped.
|
|
11
|
+
#
|
|
12
|
+
# A toolkit may be a *system* toolkit — one the engine guarantees exists, identified by a stable +key+
|
|
13
|
+
# rather than its (renameable) display name (see +Protege::SystemToolkits+). The +all_tools+ system kit
|
|
14
|
+
# is auto-maintained to hold every registered tool; +default_tools+ is auto-attached to each new agent.
|
|
15
|
+
# System toolkits can't be deleted from the dashboard. User-created toolkits leave +key+ nil.
|
|
16
|
+
class Toolkit < ApplicationRecord
|
|
17
|
+
# The stable key of the auto-maintained toolkit holding every registered tool.
|
|
18
|
+
ALL_TOOLS_KEY = 'all_tools'
|
|
19
|
+
|
|
20
|
+
# The stable key of the toolkit auto-attached to every new agent.
|
|
21
|
+
DEFAULT_TOOLS_KEY = 'default_tools'
|
|
22
|
+
|
|
23
|
+
# ── Concerns ────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
# Keep the Toolkits sidebar live (append/replace/remove).
|
|
26
|
+
include BroadcastableToolkit
|
|
27
|
+
|
|
28
|
+
# ── Serialization ─────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
serialize :member_tool_ids, coder: JSON
|
|
31
|
+
|
|
32
|
+
# ── Associations ────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
with_options dependent: :destroy do
|
|
35
|
+
has_many :agent_toolkits, class_name: 'Protege::AgentToolkit'
|
|
36
|
+
end
|
|
37
|
+
has_many :agents, through: :agent_toolkits
|
|
38
|
+
|
|
39
|
+
# ── Callbacks ─────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
before_validation :normalize_member_tool_ids
|
|
42
|
+
before_destroy :protect_system_toolkit
|
|
43
|
+
|
|
44
|
+
# ── Validations ─────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
validates :name, presence: true, uniqueness: true
|
|
47
|
+
validates :key, uniqueness: true, allow_nil: true
|
|
48
|
+
|
|
49
|
+
# ── Lookups ─────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
class << self
|
|
52
|
+
# The auto-maintained "All Tools" system toolkit, or nil before it has been synced.
|
|
53
|
+
#
|
|
54
|
+
# @return [Protege::Toolkit, nil]
|
|
55
|
+
def all_tools
|
|
56
|
+
find_by(key: ALL_TOOLS_KEY)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The "Default Tools" system toolkit auto-attached to new agents, or nil before it has been synced.
|
|
60
|
+
#
|
|
61
|
+
# @return [Protege::Toolkit, nil]
|
|
62
|
+
def default_tools
|
|
63
|
+
find_by(key: DEFAULT_TOOLS_KEY)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# ── Instance methods ──────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
# Whether this is an engine-managed system toolkit (carries a +key+) — such toolkits can't be deleted,
|
|
70
|
+
# and +all_tools+ has its membership maintained by the engine rather than the operator.
|
|
71
|
+
#
|
|
72
|
+
# @return [Boolean] true when the toolkit is keyed
|
|
73
|
+
def system?
|
|
74
|
+
key.present?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Whether this toolkit includes a given tool id.
|
|
78
|
+
#
|
|
79
|
+
# @param tool_id [Symbol, String] the tool id to test
|
|
80
|
+
# @return [Boolean] true when the id is a member
|
|
81
|
+
def covers?(tool_id)
|
|
82
|
+
member_tool_ids.map(&:to_sym).include?(tool_id.to_sym)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The member tools that still exist, as manifest entries — stale ids (naming a removed tool) are
|
|
86
|
+
# dropped, so a toolkit tolerates a member whose tool has since been deleted from the code.
|
|
87
|
+
#
|
|
88
|
+
# @return [Array<Protege::Manifest::Entry>] the live member tools
|
|
89
|
+
def member_tools
|
|
90
|
+
ids = member_tool_ids.map(&:to_sym)
|
|
91
|
+
Manifest.registered(:tool).select { |entry| ids.include?(entry.id) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# Coerce +member_tool_ids+ to a clean, deduped array of non-blank strings — so the checklist column
|
|
97
|
+
# stays tidy however it was assigned (symbols, blanks, duplicates).
|
|
98
|
+
#
|
|
99
|
+
# @return [void]
|
|
100
|
+
def normalize_member_tool_ids
|
|
101
|
+
self.member_tool_ids = Array(member_tool_ids).map(&:to_s).reject(&:blank?).uniq
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Halt destruction of a system toolkit — the engine depends on it existing (it would be recreated on
|
|
105
|
+
# the next sync anyway). User toolkits (no key) delete normally.
|
|
106
|
+
#
|
|
107
|
+
# @return [void]
|
|
108
|
+
def protect_system_toolkit
|
|
109
|
+
return unless system?
|
|
110
|
+
|
|
111
|
+
errors.add(:base, 'System toolkits cannot be deleted.')
|
|
112
|
+
throw :abort
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
data/app/models/protege/trace.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Protege
|
|
|
9
9
|
# enough to *replay* against the *current* prompts.
|
|
10
10
|
#
|
|
11
11
|
# Deliberately **isolated and minimal**: no associations, no foreign keys, and no identity columns
|
|
12
|
-
# (
|
|
12
|
+
# (agent/provider) — a trace records only what affects training, plus a human review verdict. The
|
|
13
13
|
# table can be truncated or dropped without touching anything else. +correlation_id+ + +turn_index+
|
|
14
14
|
# group the turns of one run for retry-safe dedup. Written only by the tracing subscriber
|
|
15
15
|
# (+Protege::Subscribers::Tracing+); reviewed and labeled in the dashboard.
|
|
@@ -125,6 +125,8 @@ module Protege
|
|
|
125
125
|
post = build_stream_post(uri, payload)
|
|
126
126
|
|
|
127
127
|
http.request(post) do |response|
|
|
128
|
+
raise_for_stream_status(response)
|
|
129
|
+
|
|
128
130
|
buffer = ''
|
|
129
131
|
|
|
130
132
|
response.read_body do |chunk|
|
|
@@ -137,6 +139,21 @@ module Protege
|
|
|
137
139
|
end
|
|
138
140
|
end
|
|
139
141
|
|
|
142
|
+
# Surface a non-2xx streaming response as the matching +Protege::Error+, mirroring +raise_for_status+
|
|
143
|
+
# for the non-streaming path. A streamed error (e.g. HTTP 402 insufficient credits) arrives as a normal
|
|
144
|
+
# response whose body is the JSON error, not an SSE event stream — without this it would be parsed as
|
|
145
|
+
# empty tokens and swallowed, so the run would silently produce no output and no failure event.
|
|
146
|
+
#
|
|
147
|
+
# @param response [Net::HTTPResponse] the streaming response, before its body is consumed as SSE
|
|
148
|
+
# @return [void]
|
|
149
|
+
# @raise [Protege::Error] the error class matching a non-2xx status
|
|
150
|
+
def raise_for_stream_status(response)
|
|
151
|
+
code = response.code.to_i
|
|
152
|
+
return if (200..299).cover?(code)
|
|
153
|
+
|
|
154
|
+
raise_for_status(Struct.new(:status, :body).new(code, response.read_body))
|
|
155
|
+
end
|
|
156
|
+
|
|
140
157
|
# Construct the Net::HTTP transport for an SSE request.
|
|
141
158
|
#
|
|
142
159
|
# @param uri [URI] the fully resolved chat-completions endpoint
|
data/app/resolvers/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
> User-facing docs live in `site/developer-experience/extensions/resolvers.md`; this file is the
|
|
5
5
|
> precise contract for contributors working in the engine.
|
|
6
6
|
|
|
7
|
-
A **resolver** assembles the context the LLM sees *before* an inference run. Each
|
|
7
|
+
A **resolver** assembles the context the LLM sees *before* an inference run. Each agent declares an
|
|
8
8
|
ordered chain of resolvers (`message_resolvers` for replies, `responsibility_resolvers` for scheduled
|
|
9
9
|
runs); the harness runs the chain, concatenates every resolver's output, and hands the result to the
|
|
10
10
|
provider as the opening messages of the request.
|
|
@@ -157,7 +157,7 @@ right, and the request is well-formed.
|
|
|
157
157
|
|
|
158
158
|
| Attribute | Type | Reply run | Scheduled run |
|
|
159
159
|
|------------------|-------------------------------|---------------------|---------------|
|
|
160
|
-
| `
|
|
160
|
+
| `agent` | `Protege::Agent` | present | present |
|
|
161
161
|
| `config` | `Protege::Configuration` | present (frozen) | present (frozen) |
|
|
162
162
|
| `logger` | `Logger` | present | present |
|
|
163
163
|
| `correlation_id` | `String, nil` | present | present |
|
|
@@ -173,7 +173,7 @@ they never mutate it.
|
|
|
173
173
|
## Built-in resolvers
|
|
174
174
|
|
|
175
175
|
All live in `app/resolvers/protege/` as flat `Protege::<Name>` constants (e.g.
|
|
176
|
-
`Protege::ThreadHistoryResolver`);
|
|
176
|
+
`Protege::ThreadHistoryResolver`); an agent names one by that constant. Each returns a `ModelMessage`,
|
|
177
177
|
an array of them, or `nil`.
|
|
178
178
|
|
|
179
179
|
### `ThreadHistoryResolver`
|
|
@@ -218,7 +218,7 @@ Loads ActiveRecord record(s) and contributes them as one message — the DB coun
|
|
|
218
218
|
### `LoadTextResolver`
|
|
219
219
|
|
|
220
220
|
The computed-text primitive: a block returns a `String`, contributed as one message. The clean way to
|
|
221
|
-
inject
|
|
221
|
+
inject an agent's DB-column system prompt.
|
|
222
222
|
|
|
223
223
|
- **Constructor:** `initialize(role: :system, &block)` — the **required** block receives the context and
|
|
224
224
|
returns the text.
|
|
@@ -6,22 +6,22 @@ module Protege
|
|
|
6
6
|
# prompt, a knowledge document, few-shot examples — by choosing the +role+ of the emitted message.
|
|
7
7
|
#
|
|
8
8
|
# The path is given either statically or as a block that receives the resolver context, so it can
|
|
9
|
-
# be derived from the inbound message or
|
|
9
|
+
# be derived from the inbound message or agent (e.g. a per-agent prompt file). Path resolution
|
|
10
10
|
# mirrors ordinary shell intuition, so the same resolver works in development and in any production
|
|
11
11
|
# deployment:
|
|
12
|
-
# - a relative path (+config/
|
|
12
|
+
# - a relative path (+config/agents/agent.md+, +./agent.md+) resolves against the host app root
|
|
13
13
|
# (+Rails.root+) — i.e. a file deployed with the app, which is reliably readable everywhere.
|
|
14
14
|
# - an absolute path (+/data/prompts/agent.md+) is used as-is, so an attached volume or mounted
|
|
15
15
|
# secret just needs its fully-qualified path.
|
|
16
16
|
#
|
|
17
17
|
# LoadFileResolver is a *developer-wired* resolver, never a model-callable tool — the path is author-trusted
|
|
18
|
-
# code, so even absolute reads carry the same trust as any code in the
|
|
18
|
+
# code, so even absolute reads carry the same trust as any code in the agent. A missing file is a
|
|
19
19
|
# misconfiguration and raises +Protege::ResolverFileNotFoundError+; a blank file contributes
|
|
20
20
|
# nothing (returns +nil+).
|
|
21
21
|
#
|
|
22
22
|
# resolvers do |chain|
|
|
23
|
-
# chain.use Protege::LoadFileResolver, 'config/
|
|
24
|
-
# chain.use(Protege::LoadFileResolver) { |ctx| "config/
|
|
23
|
+
# chain.use Protege::LoadFileResolver, 'config/agents/agent.md'
|
|
24
|
+
# chain.use(Protege::LoadFileResolver) { |ctx| "config/agents/#{ctx.agent.name.parameterize}.md" }
|
|
25
25
|
# end
|
|
26
26
|
class LoadFileResolver < Protege::Resolver
|
|
27
27
|
# @param path [String, nil] file to read; relative paths resolve against +Rails.root+, absolute
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
module Protege
|
|
4
4
|
# General-purpose resolver that runs a block and contributes its returned String as one context
|
|
5
5
|
# message — the computed-text sibling of +LoadFileResolver+ (text from a file) and +LoadRecordResolver+ (text from
|
|
6
|
-
# records). The block receives the resolver context, so the text can be derived from the
|
|
7
|
-
# the inbound message. The canonical use is
|
|
6
|
+
# records). The block receives the resolver context, so the text can be derived from the agent or
|
|
7
|
+
# the inbound message. The canonical use is an agent's system prompt held in a column:
|
|
8
8
|
#
|
|
9
9
|
# resolvers do |chain|
|
|
10
|
-
# chain.use(Protege::LoadTextResolver, role: :system) { |ctx| ctx.
|
|
10
|
+
# chain.use(Protege::LoadTextResolver, role: :system) { |ctx| ctx.agent.instructions }
|
|
11
11
|
# end
|
|
12
12
|
#
|
|
13
13
|
# +role+ chooses how the text enters the conversation (+:system+ for a prompt, +:user+/+:assistant+
|
|
@@ -90,7 +90,7 @@ module Protege
|
|
|
90
90
|
# @return [Array<Protege::ModelMessage>] the assistant turn followed by its results
|
|
91
91
|
def round_messages(rows)
|
|
92
92
|
assistant = message(role: :assistant, content: '', tool_calls: rows.map(&:to_provider_tool_call))
|
|
93
|
-
|
|
93
|
+
|
|
94
94
|
results = rows.map do |row|
|
|
95
95
|
message(role: :tool, tool_call_id: row.tool_call_id, content: row.replay_result(limit: REPLAY_RESULT_LIMIT))
|
|
96
96
|
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Protege
|
|
4
|
-
# Cross-
|
|
5
|
-
# ActiveRecord model, and not the
|
|
4
|
+
# Cross-agent email search behind the console's search page — a PORO service object (not an
|
|
5
|
+
# ActiveRecord model, and not the agent-scoped +search_emails+ agent tool).
|
|
6
6
|
#
|
|
7
7
|
# Each field narrows the +Message+ relation: +from+ scans the sender, +recipients+ spans the
|
|
8
8
|
# to/cc/bcc lists, +subject+ and +body+ (text + html) scan their columns, and +attachments+ matches
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Maintains the engine's *system* toolkits — the two toolkit records the framework guarantees exist,
|
|
5
|
+
# identified by a stable +key+ (see +Toolkit+):
|
|
6
|
+
#
|
|
7
|
+
# * **All Tools** (+all_tools+) — always holds every registered tool. Its membership is engine-owned:
|
|
8
|
+
# each sync rewrites it to the live tool manifest, so a tool added in code shows up automatically and
|
|
9
|
+
# an operator's edits don't stick.
|
|
10
|
+
# * **Default Tools** (+default_tools+) — auto-attached to every new agent (see +Toolable+). The engine
|
|
11
|
+
# only *seeds* it (with +send_email+, so a fresh agent can at least reply) on first creation;
|
|
12
|
+
# thereafter its membership is the operator's to curate, so later syncs leave it alone.
|
|
13
|
+
#
|
|
14
|
+
# Sync is idempotent and safe to run repeatedly. It runs like any other seed data — not at boot (writing
|
|
15
|
+
# to the DB from an initializer is an anti-pattern): the +protege:toolkits:sync+ rake task on deploy and
|
|
16
|
+
# the host's +db/seeds.rb+ in development. Both eager-load the app first so the tool manifest is complete
|
|
17
|
+
# (rake tasks don't eager-load by default), which is what makes All Tools capture every registered tool.
|
|
18
|
+
class SystemToolkits
|
|
19
|
+
class << self
|
|
20
|
+
# Ensure both system toolkits exist and refresh All Tools' membership.
|
|
21
|
+
#
|
|
22
|
+
# @return [void]
|
|
23
|
+
def sync!
|
|
24
|
+
new.sync!
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Ensure both system toolkits exist and refresh All Tools' membership.
|
|
29
|
+
#
|
|
30
|
+
# @return [void]
|
|
31
|
+
def sync!
|
|
32
|
+
sync_all_tools
|
|
33
|
+
ensure_default_tools
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# Create or update the All Tools toolkit so it holds every registered tool.
|
|
39
|
+
#
|
|
40
|
+
# @return [void]
|
|
41
|
+
def sync_all_tools
|
|
42
|
+
toolkit = Toolkit.find_or_initialize_by(key: Toolkit::ALL_TOOLS_KEY)
|
|
43
|
+
|
|
44
|
+
toolkit.name = 'All Tools'
|
|
45
|
+
toolkit.description = 'Automatically managed — always contains every registered tool, refreshed ' \
|
|
46
|
+
'on each deploy. Its members are not editable. Attach it to an agent to ' \
|
|
47
|
+
'grant the full tool catalogue.'
|
|
48
|
+
toolkit.member_tool_ids = Manifest.registered(:tool).map { |entry| entry.id.to_s }
|
|
49
|
+
toolkit.save!
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Ensure the Default Tools toolkit exists. On first creation it is seeded with +send_email+ so a new
|
|
53
|
+
# agent is immediately able to reply; on later syncs its membership is left untouched (operator-owned).
|
|
54
|
+
#
|
|
55
|
+
# @return [void]
|
|
56
|
+
def ensure_default_tools
|
|
57
|
+
toolkit = Toolkit.find_or_initialize_by(key: Toolkit::DEFAULT_TOOLS_KEY)
|
|
58
|
+
return unless toolkit.new_record?
|
|
59
|
+
|
|
60
|
+
toolkit.name = 'Default Tools'
|
|
61
|
+
toolkit.description = 'Attached to every new agent automatically. Curate its members to set the ' \
|
|
62
|
+
'baseline tools an agent starts with.'
|
|
63
|
+
toolkit.member_tool_ids = ['send_email']
|
|
64
|
+
toolkit.save!
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -5,7 +5,7 @@ module Protege
|
|
|
5
5
|
# publishes it in the LLM's tool catalog (id +:read_attachment+).
|
|
6
6
|
#
|
|
7
7
|
# The agent learns a file's blob id from the +ThreadHistoryResolver+/inbound note ("…, id: 123") or
|
|
8
|
-
# from +create_file+. Resolution is by blob id (see +Protege::StoredFile.find+);
|
|
8
|
+
# from +create_file+. Resolution is by blob id (see +Protege::StoredFile.find+); agent scoping is
|
|
9
9
|
# deferred to a later pass (see the blob-currency spec §9), so any known blob id currently resolves.
|
|
10
10
|
# Behaviour by type:
|
|
11
11
|
#
|
|
@@ -23,6 +23,8 @@ module Protege
|
|
|
23
23
|
shown to you directly to view. Other types (such as video or archives) can't be read.
|
|
24
24
|
DESC
|
|
25
25
|
|
|
26
|
+
summary 'Read a file from the conversation by its id.'
|
|
27
|
+
|
|
26
28
|
input_schema(
|
|
27
29
|
type: 'object',
|
|
28
30
|
additionalProperties: false,
|