diff --git a/docs/AGENT_TOOL_QUICKSTART.md b/docs/AGENT_TOOL_QUICKSTART.md index e86ea1f0..c3bd0ee7 100644 --- a/docs/AGENT_TOOL_QUICKSTART.md +++ b/docs/AGENT_TOOL_QUICKSTART.md @@ -30,6 +30,8 @@ The [approval hook error case](https://aruintelligence.github.io/aml-core/agent- On the visual builder, **Copy fixture JSON** copies the selected case in the same shape as `--case`, including its project-authored provenance and scope. Include your checkout commit when sharing a reproduction; the copied fixture is not evidence of a real approval or delivery. +Use **Compare with** on the visual builder to put two local outcomes, digest availability, claim counts, callback counts, and in-memory writes side by side. The address and **Copy case link** preserve the selected pair. The comparison only reads the checked-in fixture report; it does not run a tool or establish provider outcomes. + The [declined approval case](https://aruintelligence.github.io/aml-core/agent-start.html?case=approval_declined#rehearsal) requests a fixture decision and receives a refusal. The receipt blocks before any claim or tool callback. A refusal is not a grant; a later attempt needs its own authenticated decision. ## Wrap your own function tool diff --git a/docs/agent-start.html b/docs/agent-start.html index db34697d..5b3b0b8c 100644 --- a/docs/agent-start.html +++ b/docs/agent-start.html @@ -17,7 +17,7 @@ .flow{border:1px solid #517582;border-radius:25px;overflow:hidden;background:linear-gradient(130deg,#102536,#0a1525);box-shadow:0 26px 90px #0007}.bar{display:flex;justify-content:space-between;gap:12px;align-items:center;padding:17px 23px;border-bottom:1px solid var(--line);font:.74rem ui-monospace,monospace;letter-spacing:.13em;color:#c8dce5}.dots{display:flex;gap:7px}.dots i{width:8px;height:8px;background:#587586;border-radius:50%}.dots i:first-child{background:var(--cyan)}.stage{display:grid;grid-template-columns:94px minmax(0,1fr);border-bottom:1px solid var(--line)}.stage:last-child{border-bottom:0}.number{font:900 1.65rem ui-monospace,monospace;color:var(--cyan);padding:30px;border-right:1px solid var(--line);background:#061320}.content{padding:29px clamp(20px,4vw,46px)}.content h2{font-size:clamp(1.5rem,3vw,2.15rem);margin:0 0 11px}.content>p{color:var(--muted);max-width:780px;margin:0 0 18px}.code-row{display:flex;justify-content:space-between;align-items:center;gap:18px;flex-wrap:wrap}.copy{border:1px solid var(--cyan);background:#123745;color:var(--cyan);font-weight:850;padding:9px 14px;border-radius:9px}.copy:hover{background:#1d5464}.code{display:block;white-space:pre;overflow:auto;padding:19px;border:1px solid var(--line);border-radius:12px;background:#020b16;color:#dcf4f4;font:12px/1.7 ui-monospace,SFMono-Regular,monospace;margin:16px 0 11px}.small{color:var(--muted);font-size:.83rem}.small strong{color:var(--ink)}.outcomes{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:20px 0 0}.outcome{border:1px solid var(--line);border-radius:12px;padding:16px;background:#ffffff08}.outcome b{display:block;font:.73rem ui-monospace,monospace;letter-spacing:.09em;color:var(--gold)}.outcome strong{display:block;margin:8px 0;color:var(--ink);font-size:1.13rem}.outcome p{margin:0;color:var(--muted);font-size:.82rem}.sketch{max-height:500px}.status{min-height:1.3em;color:var(--gold);font-size:.81rem;margin:8px 0 0}.links{display:flex;flex-wrap:wrap;gap:12px 20px;margin-top:20px}.links a{font-weight:800}.preflight{margin-top:18px;padding:14px 16px;border:1px solid #416779;border-radius:12px;background:#83edec0b;color:#d9e9ef;font-size:.82rem}.preflight strong{color:var(--cyan)}.notice{border-left:3px solid var(--gold);padding:14px 18px;background:#f5cc7e10;color:#e1d7bf;border-radius:0 11px 11px 0;max-width:780px}.next{display:grid;grid-template-columns:repeat(3,1fr);gap:15px;margin:50px 0 75px}.next a{padding:22px;border:1px solid var(--line);border-radius:16px;background:#ffffff08;text-decoration:none}.next a:hover{border-color:var(--cyan)}.next span{display:block;color:var(--gold);font:.72rem ui-monospace,monospace;letter-spacing:.1em}.next b{display:block;color:var(--ink);font-size:1.15rem;margin:11px 0 7px}.next small{color:var(--muted);font-size:.84rem}.foot{border-top:1px solid var(--line);color:var(--muted);padding:25px 0 50px;font-size:.84rem}.foot strong{color:var(--ink)} .rehearsal{margin-top:27px;border:1px solid #557988;border-radius:19px;overflow:hidden;background:#071522}.rehearsal-head{display:flex;justify-content:space-between;gap:14px;align-items:start;flex-wrap:wrap;padding:22px;border-bottom:1px solid var(--line);background:linear-gradient(100deg,#154050,#101d33)}.rehearsal-head span{font:800 .7rem ui-monospace,monospace;letter-spacing:.13em;color:var(--cyan)}.rehearsal-head h3{font-size:1.55rem;margin:7px 0}.rehearsal-head p{color:var(--muted);margin:0;max-width:630px;font-size:.87rem}.rehearsal-head a{font-size:.78rem;font-weight:800}.receipt-legend{display:flex;flex-wrap:wrap;gap:7px;list-style:none;margin:15px 0 0;padding:0}.receipt-legend li{border:1px solid var(--line);border-radius:999px;background:#081d2c;padding:6px 9px;color:var(--muted);font-size:.68rem}.receipt-legend b{font:800 .65rem ui-monospace,monospace;letter-spacing:.05em;margin-right:5px}.receipt-legend [data-tone="go"] b{color:#a9f1c0}.receipt-legend [data-tone="stop"] b{color:var(--rose)}.receipt-legend [data-tone="hold"] b{color:var(--gold)}.receipt-legend [data-tone="preflight"] b{color:#b7aeff}.case-search{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:14px 17px;border-bottom:1px solid var(--line);background:#071c2a}.case-search label{font-weight:800;font-size:.78rem;color:var(--cyan)}.case-search input{flex:1 1 220px;min-width:0;border:1px solid #608697;border-radius:9px;background:#061320;color:var(--ink);padding:10px 12px;font:inherit;font-size:.84rem}.case-search input:focus-visible{outline:3px solid var(--gold);outline-offset:2px}.case-search p{flex-basis:100%;margin:0;color:var(--muted);font-size:.72rem}.case-picker button[hidden]{display:none}.case-picker{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;padding:17px;border-bottom:1px solid var(--line)}.case-picker button{text-align:left;border:1px solid var(--line);border-radius:11px;padding:12px;background:#ffffff07;color:var(--ink);font-weight:800;font-size:.8rem;line-height:1.3}.case-picker button span{display:block;font:700 .62rem ui-monospace,monospace;letter-spacing:.1em;color:var(--gold);margin-bottom:5px}.case-picker button[data-tone="go"] span{color:#a9f1c0}.case-picker button[data-tone="stop"] span{color:var(--rose)}.case-picker button[data-tone="preflight"] span{color:#b7aeff}.case-picker button[aria-checked="true"]{border-color:var(--cyan);background:#83edec1c;box-shadow:inset 0 0 0 1px #83edec55}.case-picker button:hover{border-color:var(--cyan)}.case-detail{padding:22px}.case-detail h4{font-size:1.35rem;margin:5px 0}.case-detail>p{color:var(--muted);margin:0 0 18px}.case-detail .eyebrow{font-size:.64rem}.attempts{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:11px}.attempt{border:1px solid var(--line);border-radius:12px;background:#ffffff06;padding:16px}.attempt[data-tone="stop"]{border-color:#a66275;background:#ff9eb20b}.attempt[data-tone="go"]{border-color:#70a98f;background:#a9f1c00b}.attempt[data-tone="hold"]{border-color:#b89d67;background:#f5cc7e0b}.attempt[data-tone="preflight"]{border-color:#9185cc;background:#b7aeff0b}.attempt small{display:block;color:var(--muted);font:700 .65rem ui-monospace,monospace;letter-spacing:.1em}.attempt strong{display:block;margin:8px 0;font-size:1.27rem}.attempt[data-tone="stop"] strong{color:var(--rose)}.attempt[data-tone="go"] strong{color:#a9f1c0}.attempt[data-tone="hold"] strong{color:var(--gold)}.attempt[data-tone="preflight"] strong{color:#b7aeff}.attempt .receipt-meaning{margin:0 0 9px;color:var(--muted);font-size:.75rem}.attempt code{display:block;color:#d4e4eb;font:.72rem/1.5 ui-monospace,monospace;overflow-wrap:anywhere}.case-metrics{display:flex;gap:9px;flex-wrap:wrap;margin-top:17px}.case-metrics span{border:1px solid var(--line);border-radius:999px;padding:7px 11px;color:#d8e9ef;font:.72rem ui-monospace,monospace}.case-detail .limit{margin:17px 0 0;color:var(--muted);font-size:.73rem}.host-move{margin-top:19px;padding:16px 18px;border-left:3px solid var(--gold);border-radius:0 10px 10px 0;background:#f5cc7e0d}.host-move span{display:block;color:var(--gold);font:800 .65rem ui-monospace,monospace;letter-spacing:.12em}.host-move p{margin:7px 0 0;color:#e4e1d9;font-size:.88rem} .attempt .trace-label{margin-top:17px}.hook-trace{list-style:none;margin:8px 0 0;padding:12px 0 0;border-top:1px solid var(--line)}.hook-trace li{position:relative;margin:0 0 8px;padding-left:17px;color:var(--muted);font-size:.73rem}.hook-trace li:before{content:"";position:absolute;left:0;top:.44em;width:6px;height:6px;border-radius:50%;background:var(--cyan)}.hook-trace li:last-child{margin:0}.hook-trace .none:before{background:#8297a5} - .case-command-row{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-top:21px}.case-command-row span{font:800 .67rem ui-monospace,monospace;letter-spacing:.12em;color:var(--cyan)}.case-actions{display:flex;gap:8px;flex-wrap:wrap}.case-command-row button{border:1px solid #608697;background:#113344;color:var(--cyan);border-radius:9px;padding:7px 11px;font-size:.77rem;font-weight:800}.case-command-row button:hover{background:#1a5264}.case-detail .case-command{margin:9px 0 0;padding:12px 14px}.case-detail .case-copy-status{min-height:1.2em;margin:5px 0 0;font-size:.73rem;color:var(--gold)} + .compare-control{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:22px}.compare-control label{font-weight:800;font-size:.8rem;color:var(--cyan)}.compare-control select{min-width:0;max-width:100%;border:1px solid #608697;border-radius:9px;background:#061320;color:var(--ink);padding:9px 12px;font:inherit;font-size:.8rem}.compare-control select:focus-visible{outline:3px solid var(--gold);outline-offset:2px}.case-comparison{margin-top:13px;border:1px solid var(--line);border-radius:12px;overflow-x:auto;background:#071c2a}.case-comparison[hidden]{display:none}.case-comparison table{border-collapse:collapse;width:100%;min-width:470px;font-size:.77rem}.case-comparison th,.case-comparison td{text-align:left;vertical-align:top;padding:10px 12px;border-bottom:1px solid var(--line);overflow-wrap:anywhere}.case-comparison thead th{color:var(--gold);font-weight:800}.case-comparison tbody th{color:var(--muted);font-weight:700;width:28%}.case-comparison tr:last-child>*{border-bottom:0}.case-command-row{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-top:21px}.case-command-row span{font:800 .67rem ui-monospace,monospace;letter-spacing:.12em;color:var(--cyan)}.case-actions{display:flex;gap:8px;flex-wrap:wrap}.case-command-row button{border:1px solid #608697;background:#113344;color:var(--cyan);border-radius:9px;padding:7px 11px;font-size:.77rem;font-weight:800}.case-command-row button:hover{background:#1a5264}.case-detail .case-command{margin:9px 0 0;padding:12px 14px}.case-detail .case-copy-status{min-height:1.2em;margin:5px 0 0;font-size:.73rem;color:var(--gold)} @media(max-width:790px){.hero{grid-template-columns:1fr}.case{max-width:520px}.next{grid-template-columns:1fr}.case-picker{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:530px){.top{flex-wrap:wrap}.top nav{gap:9px}.top nav a{font-size:.75rem}.stage{grid-template-columns:1fr}.number{padding:10px 19px;border-right:0;border-bottom:1px solid var(--line)}.bar{font-size:.63rem;flex-wrap:wrap;gap:8px 12px}.content{padding:20px}.hero h1{font-size:clamp(3rem,12vw,4.5rem)}.case-picker{grid-template-columns:1fr}.rehearsal-head,.case-detail{padding:18px}.attempts{grid-template-columns:minmax(0,1fr)}}@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}} @@ -32,7 +32,7 @@ git rev-parse HEAD node pilots/action-boundary/guarded-tool-demo.mjs node pilots/action-boundary/guarded-tool-rehearsal.mjs --check

-
HOST REHEARSAL / CONTROLLED CASES

Where does the call stop?

Choose a condition. Use arrow keys, Home, or End after focusing a case. These are recorded results from the project adapter running local fixtures, with approval and callback counts visible.

  • DISPATCHEDfixture returned
  • BLOCKEDno tool callback
  • UNKNOWNoutcome uncertain
  • HOST ERRORno plan or receipt
Jump to selected result ↓Inspect the complete report ↗
OBSERVED IN THE LOCAL REHEARSAL

Loading the report…

Checking the project-authored snapshot.

HOST DESIGN MOVE

REPRODUCE OR SHARE THIS CASE
node pilots/action-boundary/guarded-tool-rehearsal.mjs --case changed_destination

The hook trace records which fixture hooks ran; it does not expose every internal policy check. A host mapper error occurs before a plan or receipt exists, but the mapper may already have changed host state. A dispatched receipt means the fixture callback returned. An unknown receipt does not mean no side effect occurred. A claim-hook error can leave a grant spent without entering the tool callback. An approval-hook error can leave host authorization state changed. In-memory fixture writes are local markers, not provider delivery. Neither proves real approval, service delivery, or production safety.

+
HOST REHEARSAL / CONTROLLED CASES

Where does the call stop?

Choose a condition. Use arrow keys, Home, or End after focusing a case. These are recorded results from the project adapter running local fixtures, with approval and callback counts visible.

  • DISPATCHEDfixture returned
  • BLOCKEDno tool callback
  • UNKNOWNoutcome uncertain
  • HOST ERRORno plan or receipt
Jump to selected result ↓Inspect the complete report ↗
OBSERVED IN THE LOCAL REHEARSAL

Loading the report…

Checking the project-authored snapshot.

HOST DESIGN MOVE

REPRODUCE OR SHARE THIS CASE
node pilots/action-boundary/guarded-tool-rehearsal.mjs --case changed_destination

The hook trace records which fixture hooks ran; it does not expose every internal policy check. A host mapper error occurs before a plan or receipt exists, but the mapper may already have changed host state. A dispatched receipt means the fixture callback returned. An unknown receipt does not mean no side effect occurred. A claim-hook error can leave a grant spent without entering the tool callback. An approval-hook error can leave host authorization state changed. In-memory fixture writes are local markers, not provider delivery. Neither proves real approval, service delivery, or production safety.

02

Move the boundary into your host.

The real host owns the tool identity, effect, resource mapping, policy, authenticated approval, one-use claim, and service call. This sketch deliberately throws until you implement the three host functions. It imports the project preview from your checkout.

import { createGuardedToolExecute } from "./pilots/action-boundary/guarded-tool.mjs"; @@ -65,6 +65,6 @@
Scope: This is a project-authored, simulated function-tool path from a preview checkout. It does not intercept every agent tool, authenticate a human, send mail, prove delivery, or certify a production integration. ĀML™ / ĀRU Meaning Language™ is stewarded by ĀRU Intelligence Inc. · Claims and evidence.
- + diff --git a/docs/agent-start.js b/docs/agent-start.js index 7fc76a37..de14a674 100644 --- a/docs/agent-start.js +++ b/docs/agent-start.js @@ -18,6 +18,31 @@ export function rehearsalCaseUrl(currentUrl, id, report) { return url.href; } +export function rehearsalComparisonUrl(currentUrl, selectedId, comparedId, report) { + const url = new URL(rehearsalCaseUrl(currentUrl, selectedId, report)); + if (comparedId && comparedId !== selectedId && report.cases.some(item => item.id === comparedId)) { + url.searchParams.set("compare", comparedId); + } else { + url.searchParams.delete("compare"); + } + return url.href; +} + +export function rehearsalComparisonRows(selected, compared) { + const outcomes = item => item.attempts.map(attempt => + attempt.adapter_error ? "HOST ERROR (no receipt)" : attempt.execution_status.toUpperCase()).join(" → "); + const digest = item => item.attempts.every(attempt => attempt.adapter_error || attempt.proposal_sha256 === null) ? + "Unavailable" : "Present for valid proposals"; + return [ + ["Observed outcome", outcomes(selected), outcomes(compared)], + ["Proposal digest", digest(selected), digest(compared)], + ["Approval requests", selected.approval_requests, compared.approval_requests], + ["Claims consumed", selected.claims_consumed, compared.claims_consumed], + ["Fixture callbacks", selected.callback_count, compared.callback_count], + ["In-memory fixture writes", selected.fixture_state_changes, compared.fixture_state_changes] + ]; +} + export function rehearsalCasePayload(report, id) { const item = report?.cases?.find(entry => entry.id === id); if (!item) throw new RangeError("Unknown rehearsal case"); @@ -158,6 +183,36 @@ if (typeof document !== "undefined" && document.getElementById("rehearsal")) { }); $("case-picker").replaceChildren(...buttons); let selectedCase = initial; + let comparedCase = report.cases.some(item => item.id === params.get("compare") && item.id !== initial) ? + params.get("compare") : ""; + $("case-compare").replaceChildren(new Option("Choose another case…", ""), + ...report.cases.map(item => new Option(item.title, item.id))); + $("case-compare").value = comparedCase; + function renderComparison() { + const selected = report.cases.find(item => item.id === selectedCase); + const compared = report.cases.find(item => item.id === comparedCase); + $("case-comparison").hidden = !compared; + if (!compared) return; + $("compare-selected").textContent = selected.title; + $("compare-other").textContent = compared.title; + $("compare-rows").replaceChildren(...rehearsalComparisonRows(selected, compared).map(([label, left, right]) => { + const row = document.createElement("tr"); + const heading = document.createElement("th"); + heading.scope = "row"; + heading.textContent = label; + const first = document.createElement("td"); + first.textContent = left; + const second = document.createElement("td"); + second.textContent = right; + row.append(heading, first, second); + return row; + })); + } + $("case-compare").addEventListener("change", () => { + comparedCase = $("case-compare").value; + renderComparison(); + history.replaceState(null, "", rehearsalComparisonUrl(location.href, selectedCase, comparedCase, report)); + }); function filterCases() { const visible = visibleRehearsalCaseIds(report, $("case-search").value, selectedCase); buttons.forEach(button => { button.hidden = !visible.includes(button.dataset.case); }); @@ -171,6 +226,11 @@ if (typeof document !== "undefined" && document.getElementById("rehearsal")) { function show(id, updateUrl) { const item = report.cases.find(entry => entry.id === id); selectedCase = id; + if (comparedCase === id) { + comparedCase = ""; + $("case-compare").value = ""; + } + for (const option of $("case-compare").options) option.disabled = option.value === id; buttons.forEach(button => { const selected = button.dataset.case === id; button.setAttribute("aria-checked", String(selected)); @@ -199,11 +259,12 @@ if (typeof document !== "undefined" && document.getElementById("rehearsal")) { ); $("case-command").textContent = `node pilots/action-boundary/guarded-tool-rehearsal.mjs --case ${item.id}`; $("case-copy-status").textContent = ""; - if (updateUrl) history.replaceState(null, "", rehearsalCaseUrl(location.href, id, report)); + renderComparison(); + if (updateUrl) history.replaceState(null, "", rehearsalComparisonUrl(location.href, id, comparedCase, report)); } $("copy-case-link").addEventListener("click", async () => { try { - await navigator.clipboard.writeText(rehearsalCaseUrl(location.href, selectedCase, report)); + await navigator.clipboard.writeText(rehearsalComparisonUrl(location.href, selectedCase, comparedCase, report)); $("case-copy-status").textContent = "Case link copied."; } catch { $("case-copy-status").textContent = "Clipboard unavailable. Copy the address in your browser to share this case."; diff --git a/test/agent-start.test.js b/test/agent-start.test.js index 2be2fb4c..a415dd64 100644 --- a/test/agent-start.test.js +++ b/test/agent-start.test.js @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; import test from "node:test"; -import { chooseRehearsalCase, nextRehearsalCaseIndex, rehearsalCasePayload, rehearsalCaseUrl, visibleRehearsalCaseIds } from "../docs/agent-start.js"; +import { chooseRehearsalCase, nextRehearsalCaseIndex, rehearsalCasePayload, rehearsalCaseUrl, rehearsalComparisonRows, rehearsalComparisonUrl, visibleRehearsalCaseIds } from "../docs/agent-start.js"; import { selectGuardedToolCase } from "../pilots/action-boundary/guarded-tool-rehearsal.mjs"; const report = JSON.parse(readFileSync(new URL("../docs/guarded-tool-rehearsal.json", import.meta.url), "utf8")); @@ -25,6 +25,8 @@ test("the page labels the report as a fixture and offers the exact recheck comma assert.match(page, /id="case-picker"[^>]*role="radiogroup"/); assert.match(page, /