Customer operations¶
The scene¶
A customer writes in: "I booked yesterday but my plans changed, can I move my flight to next month?" An airline support agent has to answer with the fare rules, not with sympathy. This is the shape of the τ-bench airline tasks, and the measured record is sobering: in τ²-bench airline, 78% of observed failures were silent wrong-state failures — the agent changed or refunded something it should not have, with no tool error to flag it. Deterministic, read-only pre-execution gates that inspect the proposed call and current state before allowing a write raised task success from 29.6% to 42.0% (arXiv:2607.07405; research/neurosymbolic-landscape/pivot-alignment.md, C5).
That paper states Horismos's scope in its own words: such gates "do not guarantee task success, but they can deterministically prevent a known class of silent policy-violating writes."
The overlay¶
The model reads the message and proposes typed facts — fare class, hours since booking, hours to departure, segments flown. The rules decide what the fare allows. A ProposalStep can then let the model rank the legal rebooking options by what the customer asked for; it cannot invent one (Tools).
# Gallery overlay: airline change/refund eligibility — the shape of the
# tau-bench airline tasks. Illustrative fare rules, not any carrier's policy.
# The model reads the customer's message; these rules decide what the fare
# allows; a ProposalStep (doc 07) can then let the model rank the legal
# rebooking options — it cannot add one.
@prefix : <https://example.org/air#> .
@prefix horis: <https://w3id.org/horismos#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
## Domain (doc 02)
:ChangeRequest a rdfs:Class ;
rdfs:label "Change or refund request" .
:FareClass a rdfs:Class ;
horis:oneOf ( :BasicEconomy :Economy :Business ) .
:fareClass a rdf:Property ;
rdfs:domain :ChangeRequest ; rdfs:range :FareClass ;
horis:cardinality "1" .
:hoursSinceBooking a rdf:Property ;
rdfs:domain :ChangeRequest ; rdfs:range xsd:integer ;
horis:min 0 ;
horis:cardinality "0..1" .
:hoursToDeparture a rdf:Property ;
rdfs:domain :ChangeRequest ; rdfs:range xsd:integer ;
horis:min 0 ;
horis:cardinality "0..1" .
:segmentsFlown a rdf:Property ;
rdfs:domain :ChangeRequest ; rdfs:range xsd:integer ;
horis:min 0 ;
horis:cardinality "0..1" .
:Eligibility a rdfs:Class ;
horis:oneOf ( :FreeChange :ChangeWithFee :NotEligible ) .
:eligibility a rdf:Property ;
rdfs:domain :ChangeRequest ; rdfs:range :Eligibility ;
horis:cardinality "0..1" ;
horis:assignedBy horis:RulesOnly . # the model never decides what the fare allows
## Rules (doc 04)
:alreadyFlown a horis:Rule ;
horis:priority 100 ;
horis:when [
horis:subject :ChangeRequest ; horis:property :segmentsFlown ; horis:exceeds 0
] ;
horis:then [ horis:property :eligibility ; horis:value :NotEligible ] ;
horis:explain "Travel has begun — the ticket can no longer be changed or refunded" .
:freeWithin24h a horis:Rule ;
horis:priority 90 ;
horis:when [
horis:all (
[ horis:none ( [ horis:subject :ChangeRequest ; horis:property :segmentsFlown ; horis:exceeds 0 ] ) ]
[ horis:subject :ChangeRequest ; horis:property :hoursSinceBooking ; horis:atMost 24 ]
[ horis:subject :ChangeRequest ; horis:property :hoursToDeparture ; horis:atLeast 168 ]
)
] ;
horis:then [ horis:property :eligibility ; horis:value :FreeChange ] ;
horis:explain "Booked within the last 24 hours and departure is at least 7 days out — free change" .
:businessFlexible a horis:Rule ;
horis:priority 50 ;
horis:when [ horis:all (
[ horis:none ( [ horis:subject :ChangeRequest ; horis:property :segmentsFlown ; horis:exceeds 0 ] ) ]
[ horis:subject :ChangeRequest ; horis:property :fareClass ; horis:is :Business ]
) ] ;
horis:then [ horis:property :eligibility ; horis:value :FreeChange ] ;
horis:explain "Business fares are fully flexible" .
:basicLocked a horis:Rule ;
horis:priority 50 ;
horis:when [
horis:all (
[ horis:any (
[ horis:none ( [ horis:subject :ChangeRequest ; horis:property :hoursSinceBooking ; horis:atMost 24 ] ) ]
[ horis:none ( [ horis:subject :ChangeRequest ; horis:property :hoursToDeparture ; horis:atLeast 168 ] ) ]
) ]
[ horis:subject :ChangeRequest ; horis:property :fareClass ; horis:is :BasicEconomy ]
)
] ;
horis:then [ horis:property :eligibility ; horis:value :NotEligible ] ;
horis:explain "Basic economy fares cannot be changed after the 24-hour window" .
:economyFee a horis:Rule ;
horis:priority 40 ;
horis:when [
horis:all (
[ horis:none ( [ horis:subject :ChangeRequest ; horis:property :segmentsFlown ; horis:exceeds 0 ] ) ]
[ horis:any (
[ horis:none ( [ horis:subject :ChangeRequest ; horis:property :hoursSinceBooking ; horis:atMost 24 ] ) ]
[ horis:none ( [ horis:subject :ChangeRequest ; horis:property :hoursToDeparture ; horis:atLeast 168 ] ) ]
) ]
[ horis:subject :ChangeRequest ; horis:property :fareClass ; horis:is :Economy ]
[ horis:subject :ChangeRequest ; horis:property :hoursToDeparture ; horis:atLeast 24 ]
)
] ;
horis:then [ horis:property :eligibility ; horis:value :ChangeWithFee ] ;
horis:explain "Economy fare, more than 24 hours before departure — change permitted with fee" .
:economyLate a horis:Rule ;
horis:priority 40 ;
horis:when [
horis:all (
[ horis:subject :ChangeRequest ; horis:property :fareClass ; horis:is :Economy ]
[ horis:subject :ChangeRequest ; horis:property :hoursToDeparture ; horis:below 24 ]
)
] ;
horis:then [ horis:property :eligibility ; horis:value :NotEligible ] ;
horis:explain "Economy fare inside 24 hours of departure — no changes" .
# PRP-005 completeness-cover:start
# PRP-005's completeness check named the region customer-ops.md had already
# guessed at in prose: an economy fare that has not been flown, whose
# :hoursToDeparture was never recorded. :economyFee needs `atLeast 24` and
# :economyLate needs `below 24`, and a condition over an unassigned property is
# false (doc 02), so the customer matched neither rule.
#
# The default is the general economy rule, not its exception. The only condition
# that would block the change — being inside the 24-hour cutoff — is precisely the
# one that is unknown, and denying a passenger because the carrier failed to record
# its own departure time is the wrong way to resolve that.
:departureTimeUnknownEconomy a horis:Rule ;
horis:priority 30 ;
horis:when [ horis:all (
[ horis:none ( [ horis:subject :ChangeRequest ; horis:property :segmentsFlown ; horis:exceeds 0 ] ) ]
[ horis:subject :ChangeRequest ; horis:property :fareClass ; horis:is :Economy ]
[ horis:subject :ChangeRequest ; horis:property :hoursToDeparture ; horis:isEmpty true ]
) ] ;
horis:then [ horis:property :eligibility ; horis:value :ChangeWithFee ] ;
horis:explain "Economy fare, travel not begun, departure time not recorded — the late-change cutoff cannot be applied, so the standard change fee applies" .
# PRP-005 completeness-cover:end
The decision, with proof¶
from horismos import HorismosValidationError, Ontology, World
onto = Ontology.load("examples/gallery/rebooking.ttl")
world = World(onto)
def request(subject: str, **facts: object) -> None:
ref = world.assert_(":ChangeRequest", subject=subject, **facts)
proof = world.explain(ref, ":eligibility")
print(proof.render() if proof else f"{subject}: no ruling — hand to an agent")
request(":req-1", fare_class=":BasicEconomy", hours_since_booking=6,
hours_to_departure=400, segments_flown=0)
request(":req-2", fare_class=":BasicEconomy", hours_since_booking=72,
hours_to_departure=400, segments_flown=0)
request(":req-3", fare_class=":Economy", hours_since_booking=200,
hours_to_departure=30, segments_flown=0)
:eligibility = :FreeChange via freeWithin24h — "Booked within the last 24 hours and departure is at least 7 days out — free change"
├─ none of:
│ └─ segmentsFlown exceeds 0 (not held)
├─ hoursSinceBooking 6 atMost 24 (asserted by the api)
└─ hoursToDeparture 400 atLeast 168 (asserted by the api)
:eligibility = :NotEligible via basicLocked — "Basic economy fares cannot be changed after the 24-hour window"
├─ any of:
│ ├─ none of:
│ │ └─ hoursSinceBooking atMost 24 (not held)
│ └─ none of: (not held)
│ └─ hoursToDeparture 400 atLeast 168 (asserted by the api)
└─ fareClass :BasicEconomy is :BasicEconomy (asserted by the api)
:eligibility = :ChangeWithFee via economyFee — "Economy fare, more than 24 hours before departure — change permitted with fee"
├─ none of:
│ └─ segmentsFlown exceeds 0 (not held)
├─ any of:
│ ├─ none of:
│ │ └─ hoursSinceBooking atMost 24 (not held)
│ └─ none of:
│ └─ hoursToDeparture atLeast 168 (not held)
├─ fareClass :Economy is :Economy (asserted by the api)
└─ hoursToDeparture 30 atLeast 24 (asserted by the api)
The first customer gets a free change because the 24-hour rule outranks the basic-economy lock — and the proof says exactly that, which is what the agent reads back. The narration step renders it in the customer's language; the ruling never changes in translation because the prose is generated only from the proof.
A ticket that has been flown cannot be changed whatever the fare, inside 24 hours economy is locked, and no matter how persuasive the message, the model cannot assert eligibility:
request(":req-4", fare_class=":Economy", hours_since_booking=200,
hours_to_departure=10, segments_flown=0)
request(":req-5", fare_class=":Business", hours_since_booking=500,
hours_to_departure=2, segments_flown=1)
try:
world.assert_(":ChangeRequest", subject=":req-6",
fare_class=":Economy", eligibility=":FreeChange")
except HorismosValidationError as e:
print(f"[{e.constraint}] {e}")
:eligibility = :NotEligible via economyLate — "Economy fare inside 24 hours of departure — no changes"
├─ fareClass :Economy is :Economy (asserted by the api)
└─ hoursToDeparture 10 below 24 (asserted by the api)
:eligibility = :NotEligible via alreadyFlown — "Travel has begun — the ticket can no longer be changed or refunded"
└─ segmentsFlown 1 exceeds 0 (asserted by the api)
[provenance_refused] eligibility may only be assigned by rules (horis:assignedBy horis:RulesOnly); assertion came from the api
This is the prompt-injection story in miniature. A message engineered to make the model "decide" the customer is eligible has nowhere to land: the decision property is omitted from the perception schema entirely and refused at the gate if it arrives anyway. The worst an adversarial message can do is propose a wrong-but-schema-valid fact (a false booking time), which is a data-integrity risk bounded by the confidence threshold and the fallback — stated as such in Security & Trust.
Why verification matters here¶
Fare rules are the textbook case for conflict-freedom: freeWithin24h and basicLocked overlap by design and priority resolves them, but economyFee and economyLate must be disjoint, and a rule set with hundreds of fare conditions accumulates overlaps nobody intended. The check named five overlapping pairs in these seven rules — alreadyFlown against three of them, freeWithin24h against two more — each with its region and the rule that wins. All five were already resolved correctly by priority, and all five are now stated as conditions: the losing rules carry explicit segmentsFlown and 24-hour-window guards, so the rule set reads the way it behaves. Completeness reported the customer no rule covered, and it was the one this page had guessed at: a business fare that has been flown is covered — by alreadyFlown — but an economy fare with hoursToDeparture unassigned was not, because economyFee needs atLeast 24 and economyLate needs below 24. The default is now :ChangeWithFee, via departureTimeUnknownEconomy. That is a policy choice the check forced into the open rather than one it made: the only condition that would block the change is the one that is unknown, the late cutoff is the exception rather than the general rule, and denying a passenger because the carrier failed to record its own departure time is the wrong way to resolve a gap in the carrier's own data. Termination is trivial here and proven anyway.
Status and honest edges¶
- Runs today with structured input. The perception step that turns the customer's message into these facts is the Rust agent loop's
PerceptionStep(see the quickstart's mock-bridge run); theProposalStepthat ranks legal options is parsed and traced today but its execution is deferred in the current PRP-004 build. - τ-bench airline is the framework's chosen public benchmark (PRP-000). No Horismos number exists yet; the harness scaffold is a PRP-006 deliverable. The +12.4 pp figure above is the cited paper's, for its own deterministic gates — evidence for the mechanism class, not a Horismos result.
- Fare rules here are illustrative and much simpler than any carrier's.