RELAY

PSA Code Sprint 2.0 · Agentic AI in Action · August 2026

RELAY

RELAY is the exception layer a container terminal uses in the hours after a plan breaks. It reads the carrier's free-text advisory beside the structured event stream, recomputes which transhipment connections will now miss their ship, and puts every write to a terminal system in front of the duty officer as a priced, reversible decision on a tamper-evident record.

Listen to the podcast exploring this project 22 min 21 s

All terminal data SYNTHETIC 1,351 tests No API key No PORTNET integration

YARD BLOCK Y12 · TUAS-T3 MSKU 481007-3 BOX GROUP BG-0002 · CONNECTION CN-0002 AT RISK CUT-OFF 22:36 CSC PLATE
60min Margin to cut-off · CN-0002

01 · The problem

The warning arrives on a channel no rules engine reads

A transhipment connection is a group of boxes that came off one ship and has to be on another before that ship's cut-off. The carrier often knows first, and what the carrier sends is prose in an inbox. The structured notice the terminal operating system acts on can follow hours later, or never move at all.

ONE TIMELINE, TWO LANES 17:00 19:00 21:00 CUT-OFF STRUCTURED FEED arrival estimates land late and cluster the last one was already in the past CARRIER ADVISORY, FREE TEXT one advisory, hours earlier messy names, missing fields, contradictions the lead the structured feed did not give
The two lanes as the recording found them. The structured estimates land late and cluster near the cut-off; the carrier's message lands hours earlier and is prose. Shape drawn from the recorded days in evalx/results/ais-slip.json. Synthetic terminal data throughout. Scroll the figure sideways to read the whole timeline.
63 of 74

vessels that berthed 60 or more minutes after their own broadcast arrival estimate had given no structured warning of that size beforehand. Silent share 0.851, bootstrap 0.770 to 0.919.

evalx/results/ais-slip.json · deck slide 2 · recorded Singapore AIS, 24 and 25 August 2026, pseudonymised aggregates

463 of 1,106

arrival estimates on that feed were already in the past when they were sent, a share of 0.419.

evalx/results/ais-slip.json · deck slide 2

20 of 146

berthed vessels had revised that estimate by an hour or more before berthing. Where a warning did come, the median lead was 282.1 minutes.

evalx/results/ais-warning-lead.json · deck slide 2

84%

of PSA's assured vessel calls held their Assured Port Time target in 2025. RELAY is built for the rest.

PSA Annual Report 2025, Year in Review p.23 · deck slide 2


02 · How it decides

Eight steps, and each one names what owns it

The decision graph runs thirteen nodes. Eight of them carry the decision, and the colour on each says who owns the step: the language model, the twin and the solver, a rule lookup, the duty officer, or the gated write. Open a step to read what it does, the contract clause that binds it, and the code in this repository that enforces it.

language model twin and solver rule lookup duty officer gated write denial or failure

Any step can leave the path: below the completeness gate, on an action class the table does not list, or when nobody answers in 120 seconds, the episode escalates with a written summary and writes nothing.

A step can be linked directly, for example index.html?open=margin, which opens that step when the page loads. Escape closes it.

RELAY architecture: structured events and free-text advisories flow through reconciliation and a completeness gate into deterministic feasibility and a CP-SAT allocation, then through a policy table into a human approval card, a gated write and a tamper-evident ledger
The whole graph at the level the written explanation uses, including the four contracted tool servers and the ledger every step appends to. site/assets/architecture.svg · deliverables/ARCHITECTURE-AND-CONTROLS.md section 1 Scroll the figure sideways to read the whole graph.

03 · Watch it run

The recorded console, in motion

Thirty-four seconds cut from the demo recording, as a sequence of labelled zooms rather than a wide shot. The countdown board holds the connection at 41 minutes, the approval card is answered, and the board settles at 101. The console is the operator surface; every measured number on this page comes from the agent's own runs rather than from the console.

Recorded on the laptop that produced the demo, with the pricing gate off, which is the arm in which this expedite is offered as a card rather than carried as advice.
An approval card marked T1, HIGH RISK, APPROVED, for portnet.set_transfer_priority at CRITICAL, showing policy row 4, per-field confidence bars, three editable plan steps and the three options considered with their costs
The card the officer answered. Policy row 4 after the edit, per-field confidence, the plan steps and every option considered with the constraint that ruled it out.site/assets/card.jpg
An approval card marked T1, MEDIUM RISK, EXPIRED_DENIED, with a written deny-by-default escalation summary addressed to the duty supervisor
Deny by default. Nobody answered within 120 seconds, so the card expired into a denial and a written summary went to the duty supervisor. No write happened.site/assets/escalation.jpg
The console trace header showing a red CHAIN BROKEN, EVENT 2: THIS_HASH MISMATCH chip and the line: replay refused, the hash chain does not verify
A broken chain. One edited past event, and replay refuses. The rationale the model wrote is stored under its own label and is excluded from the audit record.site/assets/chain.jpg

04 · The decision, step by step

One email, six steps, forty-one minutes recovered

The hero episode as the ledger records it. Follow the six steps and watch the board change with them. Every value below appears on deck slide 5 and comes from the frozen fixture stubs/fixtures/scenario_pack_hero.json.

Step 1 · Perceive

A checked fact from the email

The model parses the free text into a schema-validated fact with allow-listed keys, labelled untrusted. Completeness scores 0.87 against a gate of 0.60.

llm_call · model_rationale · fusion_gate
Step 2 · Ingest

The twin holds the new arrival

The reconciled arrival enters the twin as a vessel fact, and both connections on that voyage are updated.

ingest_fact · vessel_eta_update
Step 3 · Decide

Margin at the ninetieth percentile

Margin to cut-off is computed at the P90 of the transfer distribution, so feasible means feasible on a bad day. CN-0002 reads 41 minutes and is AT_RISK. The model plays no part in this step.

feasibility_check · AT_RISK · 41
Step 4 · Options

Three costed options

Expedite the yard transfer at 800 US dollars, feasible. Request a cut-off extension at zero, never feasible on its own because a request is not a grant. Rebook at 2,400 US dollars.

replan_options · 3 options
Step 5 · Gate

Policy row 3, tier T1

A table lookup sets the tier and raises a card carrying a single-use token bound to the tool and its arguments, with a 120 second clock.

policy_gate row 3 · approval_requested
Step 6 · Approve and execute

Margin 41 to 101

The officer raises the expedite to CRITICAL. The twin re-simulates, the gate moves to row 4, a written justification becomes mandatory and the token re-binds to the edited arguments. The gated write lands and the twin verifies the effect.

approval_card_edited · policy_gate row 4 · action_executed · verify_effect
Countdown board live
CN-0002 · BG-0002 · MSKU 481007-3at risk outbound 0402E · cut-off 22:36 41min
CN-0001 · BG-0001feasible same voyage, same inbound vessel 390min
  1. llm_call fusion.parse_reconcile, completeness 0.87
  2. tool_call twin.ingest_fact, eta_source ADVISORY_RECONCILED
  3. tool_call twin.feasibility_check CN-0002
  4. tool_call twin.replan_options, 3 options
  5. policy_gate row 3, T1, card and token minted
  6. action_executed portnet.set_transfer_priority
Board before the advisory lands.

Values on deck slide 5 · stubs/fixtures/scenario_pack_hero.json · stubs/fixtures/trace_events.jsonl

When several connections break at once

On the cascade pack one inbound slip leaves three connections broken, one at risk and two infeasible, competing for one shift budget. CP-SAT allocates one action to each in a single solve at 5,600 US dollars, taken one card at a time. Across the solver study the joint solver saves 423 connections against a greedy comparator's 408.

deck slide 5 · evalx/results/cascade-evidence.json · twin/solver_quality.json


05 · What it refuses to do

Three refusals, in the code that carries them

The controls that matter are the ones that say no. Each excerpt below is taken from the file it names at the lines it names, and each enforces one invariant that nothing upstream can talk its way past.

Refusal 01

An action class the table does not list

The policy lookup never raises an error for an unknown tool. It returns the row 10 AUTO-DENY entry, so an action class with no established approval policy is denied before an approval card can exist, and the episode escalates with a written summary.

Contract: docs/CONTRACT.md section c, row 10. Mirrored row for row in this table. The second auto-deny branch fires on data/packs/no_policy_trigger.json, where the cheapest remedy is a berth-window shift and row 9 has no write tool by design.

stubs/policy_stub.pylines 131 to 152
131def lookup(tool: str, args: dict | None = None) -> dict:
132    """policy.lookup: deterministic tier + risk + rate row for one action.
133 
134    Never an error: an unknown tool/action class returns the row-10
135    AUTO-DENY entry (auto_deny=True), the caller must deny and escalate.
136    """
137    args = args or {}
138    for row in POLICY_TABLE:
139        if tool not in row["tools"]:
140            continue
141        pred = row.get("arg_predicate")
142        if pred is not None:
143            field, allowed = pred
144            if args.get(field) not in allowed:
145                continue
146        out = {k: v for k, v in row.items() if k not in ("tools", "arg_predicate")}
147        out["tool"] = tool
148        out["auto_deny"] = False
149        return out
150    out = dict(AUTO_DENY_ROW)
151    out["tool"] = tool
152    return out
Refusal 02

A write with no token bound to these arguments

Every write to the terminal-system mock passes one shared gate. It checks the arguments, then degraded mode server-side, then that the caller holds a scoped executor credential, then that the approval token was minted by the approval server for exactly this tool and this argument digest. The gate runs before the fault layer, so an injected GUARDRAIL_BYPASS cannot skip it.

Contract: docs/CONTRACT.md section b2 and section c. Fourteen attacks on the approval path are refused after four fixes at the token authority (evalx/results/approval-attacks.json, deck slide 7).

stubs/portnet_stub.pylines 57 to 79
57def _gate_write(tool: str, action_args: dict, approval_token, agent_credential_id,
58                idempotency_key) -> dict | None:
59    """Shared write gate. Returns an error dict, or None when the gate passes.
60 
61    Order (CONTRACT §b2): (0) args sanity -> (1) degraded-mode denial,
62    SERVER-SIDE -> (2) credential scope (CSA 2.6) -> (3) approval token
63    verified AGAINST THE APPROVAL SERVER (issuance + binding to
64    tool+args_digest + expiry). The gate runs BEFORE the fault layer, so an
65    injected GUARDRAIL_BYPASS can never skip it.
66    """
67    if not idempotency_key or not isinstance(idempotency_key, str):
68        return make_error("INVALID_ARGS", "idempotency_key must be a non-empty string")
69    degrading = degraded_mode_active()
70    if degrading is not None:
71        return make_error(
72            "DEGRADED_MODE",
73            "write refused: system is DEGRADED_TO_ADVISORY "
74            f"({degrading['fault_type']} on {degrading['target_tool']}); ALL writes are denied "
75            "while degraded, regardless of tier or approval (CONTRACT §c)",
76            context={"fault_id": degrading["fault_id"], "target_tool": degrading["target_tool"]},
77        )
78    if not approval_token:
79        return make_error(
Refusal 03

A record that has been edited

Nothing but the ledger writes the chain. Each event seals the previous hash into its own, and a head anchor records the count and the tip so a truncation is visible as well as an edit. Replay refuses a chain that does not verify.

Contract: docs/CONTRACT.md section d, 18 event types on the CSA 4.3 field set. The ledger is tamper-evident rather than immutable: an adversary with root access and the source can rewrite it, which is why production places the store outside the agent's credential scope.

stubs/ledger_stub.pylines 189 to 200
189def _append_locked(path: str, event: dict) -> dict:
190    """The critical section of append(). Caller holds both append locks."""
191    count, prev = _tip(path)
192    sealed = dict(event)
193    sealed["event_id"] = f"TRC-{count + 1:06d}"
194    sealed["prev_hash"] = prev
195    sealed["this_hash"] = chain_hash(sealed)
196    with open(path, "a", encoding="utf-8") as fh:
197        fh.write(json.dumps(sealed, sort_keys=True) + "\n")
198    _remember_tip(path, count + 1, sealed["this_hash"])
199    _write_anchor(path, count + 1, sealed["this_hash"])
200    return sealed

The tamper demonstration, five events of the hero episode

Five consecutive events from the frozen trace, with the hash each one sealed. Press the button to change one character in the third event. Every hash from that point on is recomputed, so the chain no longer verifies and replay is refused. Both hash sets were produced by this repository's own stubs.chain_hash over stubs/fixtures/trace_events.jsonl.

TRC-000009
policy_gate

policy.lookup: portnet.set_transfer_priority(EXPEDITE) -> row 3, T1 (first...

this_hashe5b166ede76d025e
TRC-000010
approval_requested

approval.request_card CARD-2026-0825-0001 (deny_after_s=120)

this_hash949645746d8c814a
TRC-000011
approval_granted

approval.decide(CARD-2026-0825-0001, APPROVED), token minted server-side, ...

this_hashf0c0629c9fd21da3
TRC-000012
human_note

note on CARD-2026-0825-0001: 'expedite ok; watch Y12 density, hold CRITICA...

this_hashfe0c84c8f68bdef6
TRC-000013
action_executed

portnet.set_transfer_priority(BG-0002, EXPEDITE) [idempotency_key=demo-tp-...

this_hash4bafe93b31defaaf
Chain verifies, 5 of 5 events

Events TRC-000009 to TRC-000013 · stubs/fixtures/trace_events.jsonl · hash function stubs/__init__.py chain_hash


06 · Evidence

Every number names the file that produced it

Each measurement below comes from a committed script, is written to a results file, and is bound to the deck by evalx/claims_check.py, which fails if a page and a results file disagree.

35 connections at risk only in an email, out of 299 at-risk scenarios. Rules saw none; all 35 went to a person. evalx/results/sweep-full-n500.final.json
320 of 320 verdicts on which an independent oracle, written from the contract text, agrees with the twin. evalx/results/validity-oracle-n320.json
56 of 56 mutation probes caught. Each named control was switched off and had to make a green test go red. 0 survived. evalx/results/mutation-probes.json
12 of 12 prompt-injection advisories produced 0 unsafe tool calls through the real graph. Nine escalate before a tool is chosen. evalx/results/fusion-ladder.json
10,000 episodes in a soak run with faults in a quarter of them: 0 writes without an approval, 0 chain failures, p99 21.7 ms. evalx/results/soak-profile.json
Catch rate on at-risk connections: the rules-only baseline reaches 0.883 with a 95 percent confidence interval of 0.846 to 0.920, the agent reaches 1.000
Over 500 seeded scenarios the rules-only lane reaches 0.883 and the agent lane 1.00. The agent's 1.00 is true by construction and is marked as such: the sweep grades at-risk with the same feasibility engine the agent calls. evalx/results/sweep-full-n500.final.json · site/assets/catch-rate.svg Scroll the figure sideways to read it.
Fusion tier ladder over 200 advisories: extraction accuracy 0.548 for the regex tier, 0.575 for the local 3B model and 0.726 for the hybrid router, with contradiction flag recall 0.471, 1.000 and 1.000
Three ways of reading the same 200 advisories. The hybrid router extracts every field exactly on 0.726 of them against 0.548 for a regex baseline and 0.575 for the local model alone, and it flags every seeded contradiction. Its gate routing, 0.850, sits below the baseline's 0.865, and the lower figure is the one published. evalx/results/fusion-ladder.json · site/assets/fusion-ladder.svg Scroll the figure sideways to read it.
The expected-value gate over the same 500 scenarios: expedites booked fall from 173 to 29, and expedite spend per rollover avoided falls from 60,835 US dollars to 19,745
The agent priced its own actions and declined most of them. Expedites booked fall from 173 to 29, and spend per rollover avoided falls from 60,835 US dollars to 19,745. The 157 at-risk connections it no longer writes for stay on the board as advice with the probability, the value and the cost attached. evalx/results/save-value-audit-n500.json and -evgate.json · site/assets/ev-gate.svg Scroll the figure sideways to read it.
A subset of the evidence sheet. The full workings, the confidence intervals, the ablations and the measurements that came out badly are in deliverables/EVIDENCE-SHEET.md.
MeasurementResultResults file
Scenarios with an at-risk connection, N = 500 seeded worlds 299 of 500 evalx/results/sweep-full-n500.final.json
Escalations raised on scenarios that were never at risk 0 of 201 same file
Save rate over the same sweep 0.579 same file
Re-planner against a greedy comparator, 567 broken connections 423 saved against 408, never worse twin/solver_quality.json
Seeded wrong recommendations, 400 generated worlds 129 of 129 caught, 0 cards, 0 writes evalx/results/oversight-probes.json
Attacks on the approval path, after four fixes 14 of 14 refused evalx/results/approval-attacks.json
External berth-allocation benchmark, 10 Port of Barcelona instances, 293 ships 10 solved, 8 proved optimal, 1 improved evalx/results/external-benchmark.json
Model tokens per advisory episode, local tier, cost imputed at zero 4,689.7 deck slide 8 · evalx/results/efficiency-eval.json
Modelled share of approval cards that expire, one officer at half availability 35.6% evalx/results/oversight-load.json
Modelled annual result for the chain under the shipped gate +87,215 US dollars, positive with probability 0.683 evalx/results/impact-model.json

What these numbers are not

The 500-scenario sweep is a simulator grading itself. Its ground truth is the twin's own feasibility check, its worlds are generated from cited public rates, and its approver never declines, so it measures the deterministic decision path under conditions it set. Two rows are true by construction and are marked wherever they appear: the agent's 1.00 catch rate and the 0 of 201 are wiring rather than findings. An independent oracle written from the contract text agrees with the twin on 320 of 320 verdicts, which narrows that objection without closing it. No desk has been staffed with this system, so the expiry rates come from a queueing model, and there is no PORTNET integration behind any of it.

deck slides 7 and 10 · evalx/results/validity-oracle-n320.json · evalx/results/oversight-load.json


07 · Limits and the ask

What is not built, and what would replace an assumption with a measurement

Limits

Integration

There is no PORTNET integration. The adapter is stubbed against a frozen contract, and berth and arrival-time changes are advisory only by design.

Data

All terminal state, connections, vessels and advisories are synthetic and labelled so in the data itself. One event carries a real arrival-drift magnitude recorded from a Singapore bounding box; the terminal state it lands on is synthetic.

The weakest component

The local 3B model invents an arrival time in 47.7% of the cases where none exists. The deterministic layer contains that, and the hybrid router brings it to 11.9%. Both figures are published rather than smoothed.

A proposal is not a grant

A rebooking counts a box as saved because it is allocated to the next sailing, while the margin against the original cut-off does not move until the carrier answers. The board labels it pending.

Scope of an episode

An episode plans across the connections its own evidence touches, not the whole board, so a board-wide cascade with unrelated causes is still several episodes.

Governance wording

RELAY is aligned with IMDA's Model AI Governance Framework for Agentic AI v1.5 and CSA's agentic addendum v1.0. It is not certified and it has not been audited.

The ask

Connection outcomes

Anonymised connection outcomes, to calibrate the twin and replace the chosen rows in the impact model, the storage charge first.

Interface documents

Interface documentation for PORTNET and the Service Allocation Tool, so RELAY runs between planning cycles rather than beside them.

Alert taxonomy

PSA's own alert taxonomy, so the tiers line up with the categories the remote operations team already uses.

First deployment

Advisory. A shadow-mode run beside the duty desk with cards shown and not executed, measured by officer acceptance rate and time to decision. T1 writes follow that evidence.

deck slide 10 · README.md · docs/SPEC.md