Intents and guards#
An intent is a named, schema-typed action on a component. It is the unit of interactivity: a human click and an agent tool call execute the exact same pipeline — guard check, input validation, run(), audit.
checkout: intent({
description: 'Pay for the cart. Has monetary side effects.',
input: schema({ note: str().optional() }),
guard: 'confirm',
ready: ({ sources }) => !sources.catalog.pending,
run: async ({ state, derived, emit, input }) => {
const order = await pay({ total: derived.total });
emit('cart.checkedOut', { orderId: order.orderId, total: order.charged });
},
}),Guards — human-in-the-loop as a keyword#
Every intent (and every api()) declares who may invoke it:
| Guard | Human click | Agent call |
|---|---|---|
auto (default) |
runs | runs unattended |
confirm |
runs | returns a proposal; a human approves or rejects |
forbidden |
runs | invisible in the manifest; invocation rejected |
Guards can be dynamic, and they see both the request context and who is asking: guard: ({ ctx, origin }) => (ctx.role === 'admin' || origin === 'human' ? 'auto' : 'confirm'). The most common origin-aware guard — instant for people, approval for agents — is ({ origin }) => (origin === 'agent' ? 'confirm' : 'auto'); the manifest advertises it as the agent will experience it (confirm). run() receives the same origin, so an intent can also branch its behavior (see Human vs. agent behavior).
Proposals#
When an agent hits a confirm intent, execution suspends and a proposal is emitted (a janux:proposal DOM event on the client; a stored proposal on the server for api tools). Approving executes the original closure exactly once; approvals cannot be replayed.
const proposal = await window.janux.call('cart.checkout'); // { status: 'proposal', id, ... }
await window.janux.approve(proposal.id); // runs itTip: give your copilot component human-only intents (guard: 'forbidden') for approve/reject, so an agent can never approve its own proposal.
Readiness#
ready declares preconditions as data. A not-ready intent is announced as ready: false in the manifest — agents know they should wait instead of failing at runtime.
Binding intents to the view#
- Click:
<button onClick={intents.addItem.with({ productId: 'p1' })}>Add</button>—.with()renders the element'sdata-inputfor you. - Form:
<form onSubmit={intents.send}><input name="text" /></form>— form fields become the input object. - Any other event the same way:
onDoubleClick={intents.open},onWheel={intents.zoom}, …
All compile to data-jxa / data-jxform / data-jxe-* markers handled by delegated listeners; no per-element handlers exist in the DOM.
Errors and audit#
Invalid input throws with precise paths (items[0].qty: below min 1) before run() executes. Every invocation — human or agent, allowed or rejected — produces an audit entry: tool, origin, guard, input, ok/error.