kestrel.markets
Version:
A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.
95 lines (91 loc) • 4.48 kB
text/typescript
/**
* # cli/render/offer — the 402/Offer as STRUCTURED output (ADR-0004).
*
* A gated remote verb returns the platform's {@link WireOfferResponse} as DATA. This
* renders it to stdout in the active mode — the conversion moment — and the caller
* exits `5` (PAYMENT_REQUIRED). It NEVER launches a browser, follows
* `human_action.url`, or auto-settles: the Offer, its proof, and the settlement
* methods are printed for the agent/human to act on. Wire shape is snake_case and
* `offer.amount` is a {@link ../backend/wire.ts Money} object (value + asset).
*/
import type { WireOfferResponse } from "../backend/wire.ts";
import type { SettlementRequired } from "../backend/hosted.ts";
import type { OutputCtx } from "../context.ts";
/** Render a 402/Offer to stdout as DATA — the conversion moment, never a browser redirect. */
export function renderOffer(p: WireOfferResponse, ctx: OutputCtx): void {
if (ctx.mode === "json") {
process.stdout.write(JSON.stringify({ schema: "kestrel.payment-required/v1", ...p }) + "\n");
return;
}
const o = p.offer;
const amount = `${o.amount.value} ${o.amount.asset}`;
const methods = p.settlement_methods.map((m) => m.method).join(",");
const human = p.human_action?.url ?? "(none)";
const proof = p.proof?.url ?? p.proof?.proof_id ?? "(none)";
if (ctx.mode === "text") {
process.stdout.write(
`payment-required\toperation=${p.operation_id}\toffer=${o.offer_id}\tscope=${o.scope.join("+")}` +
`\tamount=${amount}\tceiling=${o.ceiling}\tmethods=${methods}` +
`\thuman=${human}\tproof=${proof}\n`,
);
return;
}
// human
process.stdout.write(
`Payment required to continue operation ${p.operation_id}\n` +
` earned proof: ${proof}\n` +
` offer ${o.offer_id}: ${amount} for scope ${o.scope.join("+")} (ceiling ${o.ceiling})\n` +
` machine settlement: ${p.settlement_methods.map((m) => m.method).join(", ")}\n` +
` human claim-and-fund: ${human}\n`,
);
}
/**
* Render a SPEND-BOUNDARY suspension as DATA (kestrel-e8e7). The hosted sim paused at the Spend
* boundary (`status:"suspended"`) with a settleable Offer + a `resume` affordance inline; the
* caller exits `5` (PAYMENT_REQUIRED). Distinct from {@link renderOffer} (the pre-drive 402
* data-access gate): this boundary carries no `settlement_methods`, but it DOES carry the exact
* resume call, so the output states what is owed, for what, and how to continue — the frame is
* the docs (an agent decides its next move from this alone), never a misleading 5xx.
*/
export function renderSettlementRequired(s: SettlementRequired, ctx: OutputCtx): void {
const o = s.offer;
const amount = `${o.amount.value} ${o.amount.asset}`;
const r = s.resume;
// The wired settle URL (bead kestrel-markets-1gr8) when the platform advertised one; otherwise
// the resume affordance is the only continuation — never fabricate a settle endpoint.
const settleUrl = s.settlement?.url ?? "(settle the offer, then resume)";
if (ctx.mode === "json") {
process.stdout.write(
JSON.stringify({
schema: "kestrel.payment-required/v1",
boundary: "settlement",
operation_id: s.operationId,
offer: o,
...(s.settlement !== undefined ? { settlement: s.settlement } : {}),
resume: r,
}) + "\n",
);
return;
}
if (ctx.mode === "text") {
process.stdout.write(
`payment-required\tboundary=settlement\toperation=${s.operationId}\toffer=${o.offer_id}` +
`\tscope=${o.scope.join("+")}\tamount=${amount}\tceiling=${o.ceiling}\texpiry=${o.expiry}` +
`\tsettle=${settleUrl}\tresume=${r.method} ${r.path}\trequires_settlement=${r.requires_settlement}` +
`\tdetail=${r.detail}\n`,
);
return;
}
// human — the Offer's essentials legible: price, what-you-get (scope + ceiling), settle URL, expiry.
const settleLine =
s.settlement !== undefined
? ` settle at: ${s.settlement.method} ${s.settlement.url} (offer ${o.offer_id})\n`
: ` settle the offer ${o.offer_id}, then resume\n`;
process.stdout.write(
`Payment required to continue operation ${s.operationId} (Spend boundary)\n` +
` offer ${o.offer_id}: ${amount} for scope ${o.scope.join("+")} (ceiling ${o.ceiling}, expires ${o.expiry})\n` +
settleLine +
` then resume: ${r.method} ${r.path} (operation ${s.operationId}, cursor ${r.cursor})\n` +
` ${r.detail}\n`,
);
}