Skip to main content

app/
agents.rs

1//! Agent specs and prompt packet rules.
2//!
3//! # Operator framing
4//!
5//! Use this page when you want to understand what an AI workflow is allowed to
6//! see, draft, or summarize before a person reviews it. It is for operators,
7//! managers, and implementers checking the safety gate between helpful
8//! automation and live resort actions.
9//!
10//! The next step is to read `baseline_agent_specs` for the built-in workflow
11//! list, then open `AgentPromptPacket` to see the evidence bundle each run
12//! receives. The Rust API details below remain the implementer rules; the
13//! framing here explains how to interpret those rules operationally.
14//!
15//! This module is the app-layer rules for safe workflow automation. Agent
16//! specs describe narrow read/draft capabilities for resort workflows, and
17//! prompt packets carry the source event, typed workflow input, policy language,
18//! and expected output schema an agent may use to prepare a draft or evidence
19//! bundle. They are not live authority to mutate bookings, customer messages,
20//! schedules, deposits, incident records, or policy decisions.
21//!
22//! Specs make the app gate visible to the runtime: allowed tools are narrow
23//! read/draft surfaces, forbidden actions name unsafe authority, and default
24//! review gates remain deterministic app policy.
25//!
26//! ```
27//! use app::agents;
28//!
29//! let specs = agents::baseline_agent_specs();
30//! let manager_brief = specs
31//!     .iter()
32//!     .find(|spec| spec.name.clone().into_inner() == "manager-daily-brief")
33//!     .expect("baseline manager brief spec exists");
34//!
35//! assert!(manager_brief
36//!     .allowed_tools
37//!     .iter()
38//!     .any(|tool| tool.clone().into_inner() == "reservation-read"));
39//! assert!(manager_brief
40//!     .forbidden_actions
41//!     .iter()
42//!     .any(|action| action.clone().into_inner() == "change schedule"));
43//! assert!(manager_brief
44//!     .forbidden_actions
45//!     .iter()
46//!     .any(|action| action.clone().into_inner() == "send customer message without approval"));
47//! assert!(!manager_brief.default_review_gates.is_empty());
48//! ```
49use bon::Builder;
50use serde::{Deserialize, Serialize};
51
52use domain::{agent, policy, workflow};
53
54pub use domain::agent::{OutputSchemaName, PolicyInstruction};
55
56/// App-facing alias for the domain agent specification used by workflow automation.
57///
58/// A spec is the stable rules an agent runner receives before it builds a
59/// prompt packet: the workflow identity, business purpose, read/draft tools it
60/// may use, actions it must never take directly, and deterministic review gates
61/// that keep resort staff in control of bookings, messages, schedules, and
62/// safety-sensitive decisions.
63pub type AgentSpec = agent::Spec;
64
65/// Rules implemented by app workflow agents that prepare safe prompt packets.
66///
67/// Implementors expose their immutable [`AgentSpec`], package an event and typed
68/// input into an [`AgentPromptPacket`], then validate model/tool output before it
69/// is accepted as a draft or evidence bundle. The trait is intentionally about
70/// preparing and checking workflow artifacts; it does not grant authority to
71/// write back to Gingr, send pet-parent messages, change labor schedules, or
72/// mutate reservations.
73pub trait WorkflowAgent<Input, Output> {
74    /// Returns the agent's operational specification.
75    ///
76    /// The spec names the workflow, states its labor/customer-service purpose,
77    /// lists only the read or draft tools the runner may expose, and records the
78    /// review gates that must remain outside model control.
79    fn spec(&self) -> AgentSpec;
80    /// Builds the prompt packet for one workflow event and typed input payload.
81    ///
82    /// The returned packet should contain source event context, workflow input,
83    /// policy instructions, and output schema expectations sufficient for an
84    /// agent to draft or summarize evidence without taking live operational
85    /// action.
86    fn build_prompt_packet(
87        &self,
88        event: &workflow::Event,
89        input: Input,
90    ) -> AgentPromptPacket<Input>;
91    /// Validates a proposed workflow output before downstream app code can use it.
92    ///
93    /// Implementations should preserve deterministic policy failures and reject
94    /// unsafe output rather than treating agent text as authority to mutate
95    /// bookings, messages, schedules, incident records, or customer commitments.
96    fn validate_output(&self, output: workflow::Result<Output>) -> workflow::Result<Output>;
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
100/// Safe prompt-and-evidence packet exchanged with an automation agent.
101///
102/// Operator framing: this is the checklist packet an agent receives for one
103/// reviewable workflow run. It matters to managers because every suggested
104/// action, brief, classification, or customer-message draft should trace back to
105/// the workflow name, source event, approved input facts, policy instructions,
106/// and expected output shape recorded here.
107///
108/// Next step: compare the packet fields with the workflow's `AgentSpec` and
109/// review gates before treating any generated output as usable evidence. The
110/// field-level Rustdoc below documents the API shape; it does not grant live
111/// authority to book, charge, message, schedule, or override policy.
112///
113/// An agent prompt packet bundles the workflow identity, triggering source event,
114/// typed app input, policy instructions, and expected output schema sent to an
115/// agent runner. It is a draft/evidence gate: agents can use it to prepare a
116/// briefing, follow-up draft, classification, or manager-review packet, but the
117/// packet itself is not permission to mutate live resort systems.
118pub struct AgentPromptPacket<T> {
119    /// Workflow identifier that ties the packet to a specific agent spec.
120    ///
121    /// Examples include `manager-daily-brief`, `booking-triage`, and
122    /// `grooming-rebooking`; the name lets audits connect a packet back to the
123    /// workflow rules that defined its allowed tools and forbidden actions.
124    pub workflow_name: agent::Name,
125    /// Business goal the agent should pursue while preparing only draft output.
126    ///
127    /// This describes the labor, customer-service, safety, or data-quality
128    /// outcome for the workflow, such as summarizing labor risk or drafting a
129    /// customer follow-up, without granting authority to perform the action.
130    pub goal: agent::Purpose,
131    /// Source workflow event that caused the packet to be built.
132    ///
133    /// The event provides audit correlation for the triggering reservation,
134    /// intake, document, review, incident, or scheduled briefing path so a human
135    /// reviewer can trace why this packet exists.
136    pub event: workflow::Event,
137    /// Typed app-layer input facts available to the agent for this run.
138    ///
139    /// The payload should contain the workflow-specific request or evidence the
140    /// app has already promoted from source systems; it is context for drafting,
141    /// not authorization to repair or overwrite those source records.
142    pub input: T,
143    /// Policy instructions the runner must include in the agent context.
144    ///
145    /// These instructions state review gates, safety limits, escalation rules,
146    /// and source-grounding requirements that constrain agent drafts and make
147    /// policy compliance reviewable after the run.
148    pub policies: Vec<agent::PolicyInstruction>,
149    /// Name of the output schema expected from the agent.
150    ///
151    /// The schema name tells the runner and validator which structured draft,
152    /// classification, briefing, or evidence bundle shape to expect before any
153    /// downstream workflow code accepts the output.
154    pub output_schema_name: agent::OutputSchemaName,
155}
156
157/// Returns the baseline app agent specifications for pet-resort automation.
158///
159/// The list covers bounded workflows such as inquiry intake, booking triage,
160/// vaccine document review, manager briefings, lead conversion, grooming
161/// rebooking, reputation triage, and SOP assistance. Each spec deliberately
162/// exposes read/draft tools and review gates while forbidding direct actions
163/// such as confirming bookings, changing schedules, waiving deposits, diagnosing
164/// pets, or sending customer messages without approval.
165pub fn baseline_agent_specs() -> Vec<AgentSpec> {
166    vec![
167        spec(
168            "inquiry-intake",
169            "Extract new customer/pet/service/date details, identify missing info, and draft safe follow-up replies.",
170            ["portal-read", "crm-read", "task-create"],
171            ["confirm booking", "send sensitive message without approval"],
172            [policy::ReviewGate::CustomerMessageApproval],
173        ),
174        spec(
175            "booking-triage",
176            "Evaluate booking requests against deterministic availability, eligibility, vaccine, deposit, and policy context.",
177            ["availability-read", "policy-read", "draft-message"],
178            [
179                "invent availability",
180                "override hard policy",
181                "waive deposit",
182            ],
183            [policy::ReviewGate::ManagerApproval],
184        ),
185        spec(
186            "vaccine-document",
187            "Extract vaccine names/dates from uploaded proof and route ambiguity to human review.",
188            ["document-read", "ocr", "vaccine-policy-read"],
189            ["final approve uncertain medical document"],
190            [policy::ReviewGate::MedicalDocumentReview],
191        ),
192        spec(
193            "daily-care-update",
194            "Turn staff notes/photos into warm customer-safe update drafts with risk flags.",
195            ["care-note-read", "draft-message"],
196            [
197                "diagnose",
198                "hide concerning facts",
199                "auto-send health concern",
200            ],
201            [policy::ReviewGate::CustomerMessageApproval],
202        ),
203        spec(
204            "incident-escalation",
205            "Summarize incident facts, classify possible severity, identify missing fields, and draft manager/owner review packets.",
206            ["incident-read", "task-create", "draft-message"],
207            [
208                "close incident",
209                "diagnose",
210                "send owner message without manager approval",
211            ],
212            [
213                policy::ReviewGate::ManagerApproval,
214                policy::ReviewGate::CustomerMessageApproval,
215            ],
216        ),
217        spec(
218            "manager-daily-brief",
219            "Summarize occupancy, arrivals, labor risk, pet-care watchlist, customer follow-ups, and revenue opportunities for resort leaders.",
220            [
221                "reservation-read",
222                "labor-schedule-read",
223                "care-note-read",
224                "task-create",
225            ],
226            [
227                "invent occupancy",
228                "change schedule",
229                "send customer message without approval",
230            ],
231            [policy::ReviewGate::ManagerApproval],
232        ),
233        spec(
234            "lead-conversion",
235            "Classify inquiry intent, identify missing intake requirements, and draft next-best follow-up for boarding, daycare, grooming, or training leads.",
236            ["lead-read", "customer-read", "portal-read", "draft-message"],
237            [
238                "book reservation",
239                "promise availability",
240                "override requirements",
241            ],
242            [policy::ReviewGate::CustomerMessageApproval],
243        ),
244        spec(
245            "grooming-rebooking",
246            "Find grooming cadence opportunities, low-utilization slots, and safe customer follow-up drafts without changing calendars automatically.",
247            [
248                "grooming-history-read",
249                "availability-read",
250                "draft-message",
251            ],
252            [
253                "book grooming slot",
254                "apply discount",
255                "send message without approval",
256            ],
257            [policy::ReviewGate::CustomerMessageApproval],
258        ),
259        spec(
260            "reputation-triage",
261            "Classify review themes, identify safety/legal escalations, summarize location trends, and draft public-response packets.",
262            ["review-read", "task-create", "draft-message"],
263            [
264                "delete review",
265                "deny incident facts",
266                "publish response without approval",
267            ],
268            [
269                policy::ReviewGate::ManagerApproval,
270                policy::ReviewGate::CustomerMessageApproval,
271            ],
272        ),
273        spec(
274            "sop-policy-assistant",
275            "Answer staff policy questions from approved SOP context and route medical, refund, safety, or incident ambiguity to human review.",
276            ["policy-read", "sop-read", "task-create"],
277            ["diagnose", "approve refund", "override safety policy"],
278            [policy::ReviewGate::ManagerApproval],
279        ),
280    ]
281}
282
283fn spec<const TOOLS: usize, const FORBIDDEN: usize, const GATES: usize>(
284    name: &str,
285    purpose: &str,
286    allowed_tools: [&str; TOOLS],
287    forbidden_actions: [&str; FORBIDDEN],
288    default_review_gates: [policy::ReviewGate; GATES],
289) -> AgentSpec {
290    AgentSpec::builder()
291        .name(agent::Name::try_new(name).expect("baseline agent names are non-empty"))
292        .purpose(agent::Purpose::try_new(purpose).expect("baseline purposes are non-empty"))
293        .allowed_tools(
294            allowed_tools
295                .into_iter()
296                .map(|tool| {
297                    agent::ToolName::try_new(tool).expect("baseline tool names are non-empty")
298                })
299                .collect(),
300        )
301        .forbidden_actions(
302            forbidden_actions
303                .into_iter()
304                .map(|action| {
305                    agent::ForbiddenAction::try_new(action)
306                        .expect("baseline forbidden actions are non-empty")
307                })
308                .collect(),
309        )
310        .default_review_gates(default_review_gates.into_iter().collect())
311        .build()
312}