Exclusifbêta
Run Contract <mv-run-contract>
Un contrat d’exécution pour les longues tâches d’IA, qui devient le tableau de bord en direct de cette même tâche : une carte, deux vies. Avant : objectif, fenêtre de temps tracée comme une bande sur un axe temporel, plafond de dépenses réglable, critères de fin, limite de délégation (autorisé ✓ / non autorisé ⊘) et questions ouvertes à trancher avant le lancement ; vous signez en maintenant le bouton pendant qu’une signature manuscrite s’encre sous votre doigt, et elle se rétracte si vous relâchez trop tôt. Après : la carte se transforme sur place (FLIP), un repère "maintenant" avance dans la bande et l’étire en cas de dépassement, le plafond devient une mèche qui se consume et se met en pause pour approbation ("Autoriser +5 $" / "Arrêter"), et les points de contrôle apparaissent comme des nœuds, avec des résumés lisibles des décisions prises et "Reprendre ici".
| Catégorie | Feedback |
|---|---|
| Type | Web Component (<mv-run-contract>) |
| Statut | bêta |
| Kit | Une IA vérifiable |
| Keywords | exclusive, ai, agent, long-running, slow-ai, contract, consent, budget, cost-cap, eta, checkpoint, breadcrumbs, progress, operator, dashboard, hold-to-confirm, signature, human-in-the-loop |
When to use
- A long AI task needs agreed terms before launch: objective, time window, spending cap and allowed actions
- The same card should become a live dashboard with elapsed time, budget burn, pauses and checkpoints
- A run must pause for approval when it hits its budget and allow resuming from a checkpoint
Avoid when
- The task is short and a simple progress indicator is enough → use Progress instead
- Many agents run in parallel and need a shared overview → use Agent Lenses instead
- A plain hold-to-confirm action is needed without contract terms → use Hold Button instead
Installation
node scripts/add.mjs run-contract --out ./src/marvelousAgent IA avec le serveur MCP Marvelous UI : install_components({ slugs: ["run-contract"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Fichiers copiés (dépendances comprises) : tokens/tokens.css, core/base.css, core/canvas.js, core/dom.js, core/element.js, core/motion.js, core/observe.js, components/run-contract/run-contract.js, components/run-contract/run-contract.css.
Utilisation
Démarrage rapide, le balisage minimal qui fonctionne :
<mv-run-contract eta="25-40" budget="12">
<p slot="objective">Compare 20 product sheets on price, shipping and reviews.</p>
<ul slot="done"><li>A comparison table across 18 criteria</li></ul>
<ul slot="allow"><li>Browse public websites</li></ul>
<ul slot="deny"><li>Send emails</li></ul>
</mv-run-contract>Balisage de référence : partez de celui-ci et personnalisez-le avec les attributs, data-* et les variables CSS :
<div id="rc-demo" style="width:min(100%,42rem);margin-inline:auto">
<style>
#rc-demo .rc-bar { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; margin-top:.875rem }
#rc-demo .rc-hint { margin:0; color:var(--mv-fg-muted); font-size:.75rem }
</style>
<mv-run-contract id="rc-watch" eta="25-40" budget="12" budget-max="30" extend="5" speed="60">
<p slot="objective">Competitive watch: 20 product sheets compared on price, shipping and customer reviews.</p>
<ul slot="done">
<li>20 product sheets collected from dated public sources</li>
<li>Comparison table across 18 criteria, prices normalized with tax</li>
<li>One-page summary with the 3 biggest gaps</li>
</ul>
<ul slot="allow">
<li>Read your files in the “Market watch” folder</li>
<li>Browse public websites</li>
<li>Create a results spreadsheet</li>
</ul>
<ul slot="deny">
<li>Send emails</li>
<li>Create accounts or pay for access</li>
</ul>
<p slot="question" data-name="market" data-options="United States, International" data-answer="United States">Which market should we compare?</p>
<p slot="question" data-name="shipping" data-options="Yes (delivered price), No (list price)">Should prices include shipping?</p>
</mv-run-contract>
<div class="rc-bar">
<p class="rc-hint">Hold <kbd class="mv-kbd">Space</kbd> or the button to sign · accelerated demo: 1 s = 1 min</p>
<button class="mv-button" data-variant="outline" data-size="sm" type="button" id="rc-replay">Replay demo</button>
</div>
<script type="module">
const rc = document.getElementById("rc-watch");
await customElements.whenDefined("mv-run-contract");
// Scripted research agent: breadcrumbs are decisions, not logs.
const PLAN = [
{ at: 4, label: "Scope set", summary: "20 products kept out of 26 candidates, 6 out-of-range dropped", detail: "Exclusion rule: discontinued products and quote-only listings." },
{ at: 11, label: "Sources vetted", summary: "14 sources kept, 3 dropped (paywalled)", detail: "Subscription comparison sites are outside the delegation: no paid access was created." },
{ at: 20, label: "Sheets extracted", summary: "20 sheets collected, 2 incomplete ones retried", detail: "Two sheets missing shipping costs were completed from the retailers’ terms of sale." },
{ at: 29, label: "Prices normalized", summary: "All prices converted to delivered price with tax, 18 criteria aligned", detail: "Assumption: standard ground shipping within the contiguous United States." },
{ at: 38, label: "Summary drafted", summary: "3 major gaps found, table ready for review", detail: "The draft summary is in “Market watch / 2026-09”." },
];
const END = 43;
const cost = (m) => (m <= 33 ? m * 0.366 : 12.08 + (m - 33) * 0.2);
let timer = 0;
let next = 0;
let offset = 0; // minutes replayed after a "resume from"
let extra = 0; // money already spent before that point
const tick = () => {
if (rc.state !== "running") return;
const m = rc.elapsed;
const work = m - offset;
while (next < PLAN.length && work >= PLAN[next].at) rc.checkpoint({ ...PLAN[next++], at: m });
const done = Math.min(20, Math.floor((work / END) * 20));
rc.progress({ spent: extra + cost(work), percent: Math.min(0.99, work / END), note: `${done} of 20 sheet${done === 1 ? "" : "s"} compared` });
if (work >= END) {
clearInterval(timer);
rc.complete({ summary: "20 sheets compared across 18 criteria. Report ready: 3 major price gaps, 2 stock-outs to watch." });
}
};
const run = () => { clearInterval(timer); timer = setInterval(tick, 200); };
rc.addEventListener("mv-start", () => { next = offset = extra = 0; run(); });
rc.addEventListener("mv-resume-from", (e) => {
// The agent restarts from the chosen checkpoint: later breadcrumbs are redone from now on.
const cp = e.detail.checkpoint;
const i = PLAN.findIndex((p) => p.label === cp.label);
const workAt = i >= 0 ? PLAN[i].at : 0;
next = i + 1;
offset = rc.elapsed - workAt;
extra = rc.spent - cost(workAt);
run();
});
rc.addEventListener("mv-budget", (e) => console.info("mv-budget", e.detail.type, e.detail.spent.toFixed(2), "/", e.detail.cap));
document.getElementById("rc-replay").addEventListener("click", () => {
clearInterval(timer);
rc.reset();
});
</script>
</div>API
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
eta | string | Time window in minutes, e.g. "25-40" (read on mount when the terms property is not used). | |
budget | number | 10 | Initial spending cap. |
budget-max | number | cap × 2.5 | Upper bound of the cap slider. |
extend | number | 5 | Amount offered by "Allow +…" when the cap is reached. |
currency | string | USD | ISO 4217 currency (en-US formatting). |
speed | number | 1 | Simulated minutes per real minute for the internal clock (60: one second = one minute, handy for demos). Irrelevant when the host passes elapsed to progress(). |
hold | number (ms) | 1200 | How long the button must be held to sign. |
warn | number (0-1) | 0.8 | Budget warning threshold (fraction of the cap). |
data-state / data-phase | reflected | Current state (contract | running | paused | complete | failed | stopped) and phase (contract | run), for host styling. |
Properties
| Name | Type | Description |
|---|---|---|
terms | { objective, eta: { min, max }, budget: { cap, max, min, step, extend, currency, editable }, done: string[], allow: string[], deny: string[], questions: { id, label, options?, answer? }[] } | Contract terms (replaces the markup). Reading returns a copy, including a cap the user may have changed. Can be set before define(). |
state | string | Current state (read-only). |
answers | { [id]: string } | Answers to the clarification questions (read-only). |
elapsed | number (min) | Running time elapsed, pauses excluded (read-only). |
spent / checkpoints | number / object[] | Current spend and a copy of the checkpoints (read-only). |
Methods
| Name | Description |
|---|---|
sign() | Signs like a completed hold: refused (false) while a question is open; emits mv-sign (cancelable), then calls start(). |
start() | Turns the contract into the tracker and starts the clock (no signature when called directly). |
checkpoint({ label, summary, detail?, at? }) | Adds a checkpoint: a knot on the axis plus a conceptual breadcrumb. at in minutes (default: now). |
progress({ spent?, percent?, note?, elapsed? }) | Updates spend, progress (0-1 or 0-100), a short note ("9 of 20 sheets") and, optionally, the real elapsed time. Crossing the warning threshold or the cap triggers the warning or the pause. |
pause(reason) / resume() | Host-driven pause (clock stopped) and resume. |
extendBudget(amount?) | Raises the cap (default: extend) and resumes if the cap had paused the run. |
complete(result) / fail(error) / stop(reason?) | Ends the run: success (result.summary), failure (error.message, can resume from the last checkpoint) or stop. |
reset() | Back to the unsigned contract with the original terms and answers (reverse morph). |
Events
| Name | Description |
|---|---|
mv-sign | Contract signed, cancelable. detail: { terms, answers, signedAt }. |
mv-start | Tracker started. detail: { terms, answers, signedAt }. |
mv-checkpoint | Checkpoint added. detail: { index, label, summary, detail, at, spent }. |
mv-budget | Budget threshold. detail: { type: "warning" | "cap" | "extend", threshold?, amount?, spent, cap }. On "cap" the run is already paused: the host must suspend the agent. |
mv-pause / mv-resume / mv-stop | Requested by the user (or by the cap for mv-resume, source: "budget"): the host must act on the agent. Method calls do not emit these events. |
mv-resume-from | "Resume from here": detail { index, checkpoint }, cancelable. Otherwise later checkpoints are marked discarded and the run restarts. |
mv-complete | Done. detail: { result, elapsed, spent, checkpoints }. |
Content structure
| Name | Description |
|---|---|
objective | Element whose text is the objective (one sentence). |
done | <ul>/<ol> of done criteria (or one element per criterion). |
allow / deny | <ul> of actions the agent may / may not take. |
question | A question to settle before signing; data-options="A, B" for choices (otherwise a text field), data-name for the id, data-answer for a default answer. |
CSS classes
| Name | Description |
|---|---|
mv-run-contract-card | The generated card (an article labelled by the objective). |
mv-run-contract-axis / -band / -over / -now | Time axis: estimate band, hatched extension on overrun, "now" marker. |
mv-run-contract-fuse / -ember | Budget fuse (role=meter) and its ember. |
mv-run-contract-sign / -ink | Hold-to-sign field and the handwritten stroke. |
mv-run-contract-crumbs / -crumb | Checkpoint list (conceptual breadcrumbs). |
mv-run-contract-alert | State panel: paused for approval, done, failed, stopped. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-run-contract-accent | var(--mv-accent) | Action color (slider, latest checkpoint, primary button). |
--mv-run-contract-band | var(--mv-accent) | Tint of the estimate band. |
--mv-run-contract-ember | warning / danger mix | Ember and spark color (artistic effect). |
--mv-run-contract-ink-color | var(--mv-accent-fg) | Signature ink. |
--mv-run-contract-bg | var(--mv-surface) | Card background. |
Accessibility
Terms are a real description list (dt/dd); allowed and not-allowed actions are two labelled lists where every line has its icon and the group title carries the label (never color alone). Questions are fieldset/legend groups of native radios. The sign button is held with a pointer or Space/Enter; aria-describedby explains the hold and, while a question is open, why it is blocked (aria-disabled: it stays focusable and says why). A plain click never signs. While running, the budget is a role=meter and progress a role=progressbar, both with plain-language aria-valuetext; the time text gives elapsed time, remaining estimate and overrun (icon + label); checkpoints are a list of aria-expanded buttons revealing details and "Resume from here" (the axis knots are decorative duplicates). A polite live region announces signing, checkpoints, budget thresholds, time overrun, pauses, completion and failure. After signing, focus moves to the Pause button. Reduced motion: no animated morph, no sparks or flicker, markers step instead of gliding; the signature stays because it is the hold gauge. A single frameLoop is suspended offscreen, in hidden tabs and when idle; nothing keeps running once the element is removed.