latest
This commit is contained in:
@@ -20,6 +20,16 @@ import { createBraintrustLogger } from "../../../providers/braintrust/index.js";
|
||||
import type { AgentEngine } from "../types.js";
|
||||
|
||||
const client = new Anthropic();
|
||||
|
||||
const AUTH_FAILURE_PATTERN =
|
||||
/invalid or expired access token|request failed \(401\)/i;
|
||||
const isAutumnAuthFailure = (output: unknown) => {
|
||||
try {
|
||||
return AUTH_FAILURE_PATTERN.test(JSON.stringify(output) ?? "");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
// initLogger sets Braintrust's ambient logger so traced()/spans are recorded.
|
||||
const braintrustLogger = createBraintrustLogger();
|
||||
const braintrustEnabled = Boolean(braintrustLogger);
|
||||
|
||||
@@ -113,6 +113,22 @@ export const cmaRepo = {
|
||||
return row;
|
||||
},
|
||||
|
||||
// Forces the next ensureAutumnVault to resync tokens into the vault.
|
||||
markVaultStale: async ({
|
||||
db,
|
||||
env,
|
||||
orgId,
|
||||
}: {
|
||||
db: ChatDb;
|
||||
env: AppEnv;
|
||||
orgId: string;
|
||||
}) => {
|
||||
await db
|
||||
.update(cmaVaults)
|
||||
.set({ updated_at: 0 })
|
||||
.where(and(eq(cmaVaults.org_id, orgId), eq(cmaVaults.env, env)));
|
||||
},
|
||||
|
||||
upsertVault: async ({
|
||||
credentialId,
|
||||
db,
|
||||
|
||||
@@ -824,6 +824,7 @@ export const approvalStatusCard = ({
|
||||
),
|
||||
]
|
||||
: []),
|
||||
...(config ? [CardText(config, { style: "muted" })] : []),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
185
apps/leaf/src/ui/previewContent.ts
Normal file
185
apps/leaf/src/ui/previewContent.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import type { CardChild } from "chat";
|
||||
import { CardText, Table } from "chat";
|
||||
import { format } from "date-fns";
|
||||
|
||||
type LooseRecord = Record<string, unknown>;
|
||||
|
||||
const MAX_LINE_ITEM_ROWS = 10;
|
||||
|
||||
const asRecord = (value: unknown): LooseRecord | null =>
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as LooseRecord)
|
||||
: null;
|
||||
|
||||
const parseJson = (text: string): unknown => {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const formatEpochDate = (epochMs: number) =>
|
||||
format(epochMs, "MMM d, yyyy");
|
||||
|
||||
// Amounts are major currency units (the schema's "in cents" wording is stale —
|
||||
// the dashboard renders these values directly).
|
||||
const formatMoney = (amount: number, currency: string) => {
|
||||
try {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
currency: currency.toUpperCase(),
|
||||
currencyDisplay: "narrowSymbol",
|
||||
style: "currency",
|
||||
}).format(amount);
|
||||
} catch {
|
||||
return `$${amount.toFixed(2)}`;
|
||||
}
|
||||
};
|
||||
|
||||
// Unwraps MCP transport shapes around the preview payload: JSON strings,
|
||||
// [{text}] content arrays, {content} results, and the {preview, pending} wrapper.
|
||||
export const parsePreviewPayload = (preview: unknown): LooseRecord | null => {
|
||||
if (typeof preview === "string") {
|
||||
const parsed = parseJson(preview.trim());
|
||||
return parsed ? parsePreviewPayload(parsed) : null;
|
||||
}
|
||||
if (Array.isArray(preview)) {
|
||||
for (const entry of preview) {
|
||||
const record = asRecord(entry);
|
||||
if (typeof record?.text !== "string") continue;
|
||||
const parsed = parsePreviewPayload(record.text);
|
||||
if (parsed) return parsed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const record = asRecord(preview);
|
||||
if (!record) return null;
|
||||
if (Array.isArray(record.content)) return parsePreviewPayload(record.content);
|
||||
if ("preview" in record) return parsePreviewPayload(record.preview);
|
||||
return record;
|
||||
};
|
||||
|
||||
const UPDATE_INTENT_LABELS: Record<string, string> = {
|
||||
cancel_end_of_cycle: "Cancel at end of cycle",
|
||||
cancel_immediately: "Cancel immediately",
|
||||
uncancel: "Uncancel",
|
||||
update_plan: "Update plan",
|
||||
update_quantity: "Update quantity",
|
||||
};
|
||||
|
||||
const lineItemRows = ({
|
||||
lineItems,
|
||||
currency,
|
||||
}: {
|
||||
lineItems: unknown[];
|
||||
currency: string;
|
||||
}) => {
|
||||
const items = lineItems.flatMap((item) => {
|
||||
const record = asRecord(item);
|
||||
return typeof record?.display_name === "string" &&
|
||||
typeof record.total === "number"
|
||||
? [{ name: record.display_name, total: record.total }]
|
||||
: [];
|
||||
});
|
||||
|
||||
const rows = items
|
||||
.slice(0, MAX_LINE_ITEM_ROWS)
|
||||
.map((item) => [item.name, formatMoney(item.total, currency)]);
|
||||
if (items.length > MAX_LINE_ITEM_ROWS) {
|
||||
rows.push([`+${items.length - MAX_LINE_ITEM_ROWS} more items`, ""]);
|
||||
}
|
||||
return rows;
|
||||
};
|
||||
|
||||
// attach / createSchedule / updateSubscription previews all share the
|
||||
// BillingPreviewResponse shape (line_items, total, currency, next_cycle).
|
||||
// Rendered receipt-style: one table holding line items AND total rows.
|
||||
const billingPreviewElements = (payload: LooseRecord): CardChild[] => {
|
||||
const currency =
|
||||
typeof payload.currency === "string" ? payload.currency : "usd";
|
||||
const rows = lineItemRows({
|
||||
lineItems: payload.line_items as unknown[],
|
||||
currency,
|
||||
});
|
||||
|
||||
const nextCycle = asRecord(payload.next_cycle);
|
||||
const intentLabel =
|
||||
typeof payload.intent === "string"
|
||||
? UPDATE_INTENT_LABELS[payload.intent]
|
||||
: undefined;
|
||||
|
||||
rows.push(["Due now", formatMoney(payload.total as number, currency)]);
|
||||
if (
|
||||
typeof nextCycle?.total === "number" &&
|
||||
typeof nextCycle.starts_at === "number"
|
||||
) {
|
||||
rows.push([
|
||||
`Next cycle · ${formatEpochDate(nextCycle.starts_at)}`,
|
||||
formatMoney(nextCycle.total, currency),
|
||||
]);
|
||||
}
|
||||
|
||||
const notes = [
|
||||
intentLabel ? `Change: ${intentLabel}` : null,
|
||||
payload.redirect_to_checkout === true
|
||||
? "Customer pays via checkout link"
|
||||
: null,
|
||||
].filter((note): note is string => Boolean(note));
|
||||
|
||||
return [
|
||||
Table({ align: ["left", "right"], headers: ["Item", "Amount"], rows }),
|
||||
...(notes.length
|
||||
? [CardText(notes.join(" · "), { style: "muted" })]
|
||||
: []),
|
||||
];
|
||||
};
|
||||
|
||||
const balancePreviewElements = (payload: LooseRecord): CardChild[] | null => {
|
||||
const request = asRecord(payload.request);
|
||||
if (!request) return null;
|
||||
|
||||
const reset = asRecord(request.reset);
|
||||
const fields = [
|
||||
["Feature", request.feature_id],
|
||||
[
|
||||
"Grant",
|
||||
request.unlimited === true ? "Unlimited" : request.included_grant,
|
||||
],
|
||||
[
|
||||
"Expires",
|
||||
typeof request.expires_at === "number"
|
||||
? formatEpochDate(request.expires_at)
|
||||
: null,
|
||||
],
|
||||
[
|
||||
"Resets",
|
||||
typeof reset?.interval === "string"
|
||||
? `Every ${typeof reset.interval_count === "number" && reset.interval_count > 1 ? `${reset.interval_count} ${reset.interval}s` : reset.interval}`
|
||||
: null,
|
||||
],
|
||||
].flatMap(([label, value]) =>
|
||||
typeof value === "string" || typeof value === "number"
|
||||
? [`**${label}** ${value}`]
|
||||
: [],
|
||||
);
|
||||
|
||||
return [
|
||||
...(fields.length ? [CardText(fields.join("\n"))] : []),
|
||||
...(typeof payload.impact === "string"
|
||||
? [CardText(payload.impact, { style: "muted" })]
|
||||
: []),
|
||||
];
|
||||
};
|
||||
|
||||
/** Structured card body for a preview payload, or null to fall back to text. */
|
||||
export const previewElements = (preview: unknown): CardChild[] | null => {
|
||||
const payload = parsePreviewPayload(preview);
|
||||
if (!payload) return null;
|
||||
if (Array.isArray(payload.line_items) && typeof payload.total === "number") {
|
||||
return billingPreviewElements(payload);
|
||||
}
|
||||
if (payload.action === "createBalance") {
|
||||
return balancePreviewElements(payload);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
157
apps/leaf/tests/unit/ui/previewContent.test.ts
Normal file
157
apps/leaf/tests/unit/ui/previewContent.test.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { approvalCard } from "../../../src/ui/blocks.js";
|
||||
import {
|
||||
parsePreviewPayload,
|
||||
previewElements,
|
||||
} from "../../../src/ui/previewContent.js";
|
||||
|
||||
const attachPreview = {
|
||||
object: "attach_preview",
|
||||
customer_id: "cus_1",
|
||||
currency: "usd",
|
||||
line_items: [
|
||||
{
|
||||
display_name: "Pro Checkout",
|
||||
description: "Pro Checkout - Base Price",
|
||||
subtotal: 20,
|
||||
total: 20,
|
||||
discounts: [],
|
||||
},
|
||||
{
|
||||
display_name: "Messages",
|
||||
description: "Prepaid usage",
|
||||
subtotal: 5.5,
|
||||
total: 5.5,
|
||||
discounts: [],
|
||||
},
|
||||
],
|
||||
subtotal: 25.5,
|
||||
total: 25.5,
|
||||
next_cycle: { starts_at: 1812731225000, subtotal: 40, total: 40 },
|
||||
redirect_to_checkout: false,
|
||||
};
|
||||
|
||||
describe("parsePreviewPayload", () => {
|
||||
test("unwraps MCP content arrays of JSON text", () => {
|
||||
const payload = parsePreviewPayload([
|
||||
{ type: "text", text: JSON.stringify(attachPreview) },
|
||||
]);
|
||||
expect(payload?.customer_id).toBe("cus_1");
|
||||
});
|
||||
|
||||
test("unwraps the agent {preview, pending} wrapper", () => {
|
||||
const payload = parsePreviewPayload({
|
||||
preview: attachPreview,
|
||||
pending: true,
|
||||
message: "Preview ready",
|
||||
});
|
||||
expect(payload?.object).toBe("attach_preview");
|
||||
});
|
||||
|
||||
test("returns null for model prose", () => {
|
||||
expect(parsePreviewPayload("I'll preview this now!")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("previewElements", () => {
|
||||
test("renders billing previews as a line item table with totals", () => {
|
||||
const elements = previewElements(attachPreview);
|
||||
const json = JSON.stringify(elements);
|
||||
|
||||
expect(elements?.[0]?.type).toBe("table");
|
||||
expect(json).toContain("Pro Checkout");
|
||||
expect(json).toContain("$20.00");
|
||||
expect(json).toContain("$5.50");
|
||||
expect(json).toContain("Due now");
|
||||
expect(json).toContain("Next cycle");
|
||||
expect(json).toContain("$40.00");
|
||||
});
|
||||
|
||||
test("labels update subscription intents", () => {
|
||||
const json = JSON.stringify(
|
||||
previewElements({
|
||||
...attachPreview,
|
||||
object: "update_subscription_preview",
|
||||
intent: "cancel_end_of_cycle",
|
||||
}),
|
||||
);
|
||||
expect(json).toContain("Cancel at end of cycle");
|
||||
});
|
||||
|
||||
test("renders createBalance local previews as fields", () => {
|
||||
const json = JSON.stringify(
|
||||
previewElements({
|
||||
action: "createBalance",
|
||||
request: {
|
||||
customer_id: "cus_1",
|
||||
feature_id: "credits",
|
||||
included_grant: 500,
|
||||
expires_at: 1812731225000,
|
||||
},
|
||||
impact: "Creates a standalone balance grant.",
|
||||
}),
|
||||
);
|
||||
expect(json).toContain("credits");
|
||||
expect(json).toContain("500");
|
||||
expect(json).toContain("Expires");
|
||||
expect(json).toContain("standalone balance grant");
|
||||
});
|
||||
});
|
||||
|
||||
describe("approvalCard with structured previews", () => {
|
||||
test("uses the structured renderer instead of text scraping", () => {
|
||||
const card = approvalCard({
|
||||
id: "approval_1",
|
||||
toolName: "attach",
|
||||
toolArgs: { request: { customer_id: "cus_1", plan_id: "pro" } },
|
||||
preview: attachPreview,
|
||||
});
|
||||
const json = JSON.stringify(card);
|
||||
|
||||
expect(json).toContain("table");
|
||||
expect(json).toContain("Due now");
|
||||
expect(card.children.at(-1)?.type).toBe("actions");
|
||||
});
|
||||
|
||||
test("moves environment to subtitle and config to a muted line", () => {
|
||||
const card = approvalCard({
|
||||
id: "approval_1",
|
||||
env: "sandbox" as never,
|
||||
toolName: "attach",
|
||||
toolArgs: {
|
||||
request: {
|
||||
customer_id: "cus_1",
|
||||
plan_id: "pro",
|
||||
redirect_mode: "if_required",
|
||||
invoice_mode: { enabled: true, finalize: false },
|
||||
},
|
||||
},
|
||||
preview: attachPreview,
|
||||
});
|
||||
|
||||
expect(card.subtitle).toContain("Sandbox");
|
||||
const muted = card.children.filter(
|
||||
(child) => child.type === "text" && child.style === "muted",
|
||||
);
|
||||
const mutedJson = JSON.stringify(muted);
|
||||
expect(mutedJson).toContain("Redirect: if_required");
|
||||
expect(mutedJson).toContain("Invoice: enabled, draft invoice");
|
||||
expect(JSON.stringify(card.children)).not.toContain("Environment");
|
||||
});
|
||||
|
||||
test("shows schedule start dates from tool args", () => {
|
||||
const card = approvalCard({
|
||||
id: "approval_2",
|
||||
toolName: "createSchedule",
|
||||
toolArgs: {
|
||||
request: {
|
||||
customer_id: "cus_1",
|
||||
plan_id: "pro",
|
||||
starts_at: 1812731225000,
|
||||
},
|
||||
},
|
||||
preview: attachPreview,
|
||||
});
|
||||
expect(JSON.stringify(card)).toContain("Starts");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user