/** * The tools interactive chat gets. * * Two layers, and the distinction between them is the whole design. * * FOCUSED tools answer where the user already is: the record the panel was * opened from, or the page it is docked on. They take no id, because the id is * the context, and a tool that could pivot would let the model wander off the * thing the user is looking at. * * LOOKUP tools are the opposite, and exist because the focused layer capped * every conversation at one question. "Compare Halcyon and Northwind", "which * customer has the nearest renewal", "what can we buy H200 for" are all * questions about rows nobody handed Piggy, and until it could find one by name * the only honest answer was that it could not look. * * Lookup tools are offered on every message, so each is a permanent tax on the * prompt and one more thing a 30B model can choose wrongly. Four earned that — * see the note above `createLookupPigTools` for what was declined and why. */ import { PIGGY_RECORD_TYPES, formatCents, isPageContext, type PiggyRecordType, } from '@pig/core'; import { accounts, allocations, capacityCommitments, contacts, contractObligations, contracts, demandDeals, slaMetricTargets, slaTerms, supplyDeals, type Contract, type Database, type InventoryListing, } from '@pig/db'; import { CapacityService } from '@pig/api/src/services/capacity'; import { renewalAlarm } from '@pig/api/src/services/contracts'; import { and, asc, count, eq, gt, ilike, inArray, isNotNull, isNull } from 'drizzle-orm'; import { z } from 'zod'; import type { PiggyChatContext } from './chat'; import { createAccountLifecycleTool } from './lifecycle-tools'; import { atLeast, createPagePigTools, resultScope, type ResultScope } from './page-tools'; import { defineTool, type AgentTool } from './provider'; const noInput = z.object({}).strict(); /** * The per-collection cap on a record read. * * Named rather than repeated as a literal because the scope below reports it: * a related list that came back exactly full is a list that was probably cut, * and a cut list the reader cannot see is how "this account has 100 deals" * gets said about an account with three hundred. */ const RELATED_LIMIT = 100; /** * The scope of a record read. * * Unlike the page tools, a record read filters nothing — it enumerates what * belongs to one row — so `matched` equals `total` and the sentence is not * hedged. What it must still say is the boundary, because the failure here is * the same shape as the /capacity one measured in production: asked how many * deals are on the book while an account is in focus, a model with only this * payload counts the four in front of it. `totalLabel` therefore names the * record and says the figures stop there. */ function recordScope(subject: string, collections: Record): ResultScope { const entries = Object.entries(collections); const rows = entries.reduce((sum, [, list]) => sum + list.length, 0); const capped = entries.some(([, list]) => list.length >= RELATED_LIMIT); const breakdown = entries.map(([label, list]) => `${list.length} ${label}`).join(', '); return resultScope({ covers: `belong to ${subject}`, matched: rows, total: rows, totalLabel: `record(s) belonging to ${subject} and to no other — ${breakdown}; these are that record's own figures, never book-wide totals`, listed: rows, filters: capped ? { rowCapPerCollection: RELATED_LIMIT } : {}, truncated: capped, }); } /** * Interactive chat gets one scoped read tool for where it is, plus the lookup * layer, and no ambient access. * * A record context gets `pig_get_record`, which takes no id and so always reads * the row the user opened. A page context gets the single tool that answers * that page — and never `pig_get_record`, because there is no record to read * and a tool that would throw is a wasted turn out of four. */ export function createInteractivePigTools( db: Database, context: PiggyChatContext | undefined, ): AgentTool[] { return [...focusedPigTools(db, context), ...createLookupPigTools(db)]; } function focusedPigTools(db: Database, context: PiggyChatContext | undefined): AgentTool[] { // No context is the dashboard case by another name: the same bounded // workspace overview, rather than a second definition that could drift. if (!context) return createPagePigTools(db, '/'); if (isPageContext(context)) return createPagePigTools(db, context.route); const focused = defineTool({ name: 'pig_get_record', description: 'Read the PIG record currently in focus and its directly related commercial data. ' + 'This tool accepts no id; use pig_get_record_by_id to read a different record.', inputSchema: noInput, execute: async () => readFocusedRecord(db, context), }); if (context.type === 'account') return [focused, createAccountLifecycleTool(db, context.id)]; return [focused]; } type PiggyRecordContext = Exclude; /** * One not-found message for both entry points. * * `readFocusedRecord` is now reached from a context the panel supplied AND from * an id the model chose, so "the account in focus no longer exists" was wrong * half the time — and wrong in the direction that makes a model retry rather * than correct the id it invented. */ function missingRecord(type: PiggyRecordType, id: string): Error { return new Error(`No ${type} record exists with id ${id}.`); } async function readFocusedRecord(db: Database, context: PiggyRecordContext): Promise { if (context.type === 'account') { const [account] = await db.select().from(accounts).where(eq(accounts.id, context.id)).limit(1); if (!account) throw missingRecord(context.type, context.id); const [people, demand, supply, paperwork] = await Promise.all([ db.select().from(contacts).where(eq(contacts.accountId, context.id)).limit(RELATED_LIMIT), db.select().from(demandDeals).where(eq(demandDeals.accountId, context.id)).limit(RELATED_LIMIT), db.select().from(supplyDeals).where(eq(supplyDeals.accountId, context.id)).limit(RELATED_LIMIT), db.select().from(contracts).where(eq(contracts.accountId, context.id)).limit(RELATED_LIMIT), ]); return { scope: recordScope(`the account ${account.name}`, { 'contact(s)': people, 'demand deal(s)': demand, 'supply deal(s)': supply, 'contract(s)': paperwork, }), account, contacts: people, demandDeals: demand, supplyDeals: supply, contracts: paperwork, }; } if (context.type === 'contact') { const [contact] = await db.select().from(contacts).where(eq(contacts.id, context.id)).limit(1); if (!contact) throw missingRecord(context.type, context.id); const [account] = contact.accountId ? await db.select().from(accounts).where(eq(accounts.id, contact.accountId)).limit(1) : []; return { scope: recordScope(`the contact ${contact.fullName}`, { 'account(s)': account ? [account] : [], }), contact, account: account ?? null, }; } if (context.type === 'demand_deal') { const [deal] = await db.select().from(demandDeals).where(eq(demandDeals.id, context.id)).limit(1); if (!deal) throw missingRecord(context.type, context.id); const [account] = await db.select().from(accounts).where(eq(accounts.id, deal.accountId)).limit(1); const reservations = await db .select() .from(allocations) .where(eq(allocations.demandDealId, deal.id)) .limit(RELATED_LIMIT); return { scope: recordScope(`the demand deal ${deal.name}`, { 'allocation(s)': reservations, 'account(s)': account ? [account] : [], }), deal, account: account ?? null, allocations: reservations, }; } if (context.type === 'supply_deal') { const [deal] = await db.select().from(supplyDeals).where(eq(supplyDeals.id, context.id)).limit(1); if (!deal) throw missingRecord(context.type, context.id); const [account] = await db.select().from(accounts).where(eq(accounts.id, deal.accountId)).limit(1); const commitments = await db .select() .from(capacityCommitments) .where(eq(capacityCommitments.supplyDealId, deal.id)) .limit(RELATED_LIMIT); return { scope: recordScope(`the supply deal ${deal.name}`, { 'capacity commitment(s)': commitments, 'account(s)': account ? [account] : [], }), deal, account: account ?? null, commitments, }; } if (context.type === 'commitment') { const [commitment] = await db .select() .from(capacityCommitments) .where(eq(capacityCommitments.id, context.id)) .limit(1); if (!commitment) throw missingRecord(context.type, context.id); const reservations = await db .select() .from(allocations) .where(eq(allocations.capacityCommitmentId, commitment.id)) .limit(RELATED_LIMIT); return { scope: recordScope(`the capacity commitment ${commitment.name}`, { 'allocation(s)': reservations, }), commitment, allocations: reservations, }; } const [contract] = await db.select().from(contracts).where(eq(contracts.id, context.id)).limit(1); if (!contract) throw missingRecord(context.type, context.id); const [serviceLevels, obligations] = await Promise.all([ db.select().from(slaTerms).where(eq(slaTerms.contractId, contract.id)).limit(10), db .select() .from(contractObligations) .where(eq(contractObligations.contractId, contract.id)) .limit(RELATED_LIMIT), ]); const metrics = serviceLevels[0] ? await db .select() .from(slaMetricTargets) .where(eq(slaMetricTargets.slaTermId, serviceLevels[0].id)) .limit(RELATED_LIMIT) : []; return { scope: recordScope(`the contract ${contract.title}`, { 'SLA term(s)': serviceLevels, 'SLA metric target(s)': metrics, 'obligation(s)': obligations, }), contract, slaTerms: serviceLevels, slaMetricTargets: metrics, obligations, }; } // --------------------------------------------------------------------------- // The lookup layer // --------------------------------------------------------------------------- /** Rows any one search may carry back per table, before ranking. */ const SEARCH_PER_TYPE = 5; /** Rows a search result may carry in total, after ranking. */ const SEARCH_RESULTS = 12; /** The longest name fragment a model may send. Long enough for any real name. */ const SEARCH_QUERY_MAX = 64; /** Rows a ranked list may carry. Everything above it is reported as a count. */ const EXEMPLARS = 8; /** Bound on an internal scan. Wide enough for a real book, still finite. */ const SCAN_LIMIT = 200; /** * The tools that are not about where the user is standing. * * Four, chosen against a fixed budget: every entry here is in the prompt of * every message and is another candidate for a small model to pick wrongly. * * `pig_search_records` and `pig_get_record_by_id` are the pair that lifts the * one-question ceiling — find a row by name, then open it. `readFocusedRecord` * does the opening, so a record fetched by id is shaped exactly like the record * the panel was opened from and the model has one shape to learn, not two. * * `pig_list_renewals` exists because the renewal deadline is expiry minus * notice days, which nothing in a record payload states outright: given the raw * contract a model has to do date arithmetic it is bad at, and a notice window * that quietly opened last week is the most expensive thing in this book to * miss. The calendar tool does surface renewal notices, but only on /calendar, * mixed into thirteen other kinds, and without the contract ids. * * `pig_list_inventory` reads what providers are currently offering. Nothing * else can see that table at all, and "what would this cost us to buy today" * is the supply half of every pricing conversation. * * Declined, deliberately: * * - A commitment-comparison tool. `pig_search_records` plus two * `pig_get_record_by_id` calls already answer it inside the four-turn budget, * and `pig_get_margin_summary` already ranks live blocks by margin. A fifth * tool would be a fifth wrong choice for a question two calls cover. * - Anything over `activities`. There are 185 of them, they are prose, and a * bounded slice of somebody's notes is the fastest way to spend a 1024-token * answer on transcription. The lifecycle tool already carries the one * activity fact that changes a decision — when the account last moved. * - Contacts in the search index. A person is not a commercial record, the * account read already returns its contacts, and every extra searched table * dilutes the twelve result slots the model actually reads. */ export function createLookupPigTools(db: Database): AgentTool[] { // Two things about the optional parameters below are load-bearing and // invisible in TypeScript, both found by printing what the model is actually // sent (`zodToJsonSchema(..., { target: 'openAi' })`). // // They are `.nullish()`, not `.optional()`. The OpenAI target emits an // optional field as required-and-nullable, so a model that follows the schema // it was given sends `{"side": null}` — which `.optional()` rejects, turning a // correct call into a failed tool result. // // And `.describe()` comes BEFORE `.nullish()`. Applied after, the description // is attached to the wrapper and dropped from the emitted schema, so the // sentence explaining the parameter never reaches the model at all. return [ defineTool({ name: 'pig_search_records', description: 'Find PIG records by name when they are not already in focus. Matches the name or title ' + 'of accounts, demand deals, supply deals, contracts and capacity commitments, ' + 'case-insensitively, on a fragment. Returns each match with its type and id, for ' + 'pig_get_record_by_id. Names only: this does not search notes, activities or people.', inputSchema: z .object({ query: z .string() .trim() .min(2) .max(SEARCH_QUERY_MAX) .describe( `Name fragment, 2 to ${SEARCH_QUERY_MAX} characters. Use the distinctive word, not a whole sentence: "Halcyon", not "the Halcyon Research account".`, ), }) .strict(), execute: async ({ query }) => searchRecords(db, query), }), defineTool({ name: 'pig_get_record_by_id', description: 'Read one PIG record by type and id, with its directly related commercial data. Use it ' + 'to open a result from pig_search_records. Ids must come from a tool result; never ' + 'invent one.', inputSchema: z .object({ type: z .enum(PIGGY_RECORD_TYPES) .describe('Record type, exactly as pig_search_records reported it.'), id: z.string().uuid().describe('Record id from a previous tool result.'), }) .strict(), execute: async ({ type, id }) => readFocusedRecord(db, { type, id }), }), defineTool({ name: 'pig_list_renewals', description: 'List executed contracts that have not yet expired, ordered by the nearest deadline: ' + 'the renewal-notice date where the contract auto-renews, otherwise the expiry date. ' + 'renewalState is "due" when the notice window is already open, "scheduled" when it is ' + 'still ahead, "not_applicable" when the contract does not auto-renew.', inputSchema: z .object({ side: z .enum(['demand', 'supply']) .describe('demand for customer paper, supply for provider paper. null for both.') .nullish(), }) .strict(), execute: async ({ side }) => listRenewals(db, side ?? undefined), }), defineTool({ name: 'pig_list_inventory', description: 'List the GPU capacity third-party providers currently offer for purchase, cheapest ' + 'first: provider, region, interconnect, stock level and on-demand price per GPU-hour. ' + 'This is capacity on offer, not capacity PIG already owns — what PIG owns is a capacity ' + 'commitment.', inputSchema: z .object({ gpuType: z .string() .trim() .min(1) .max(24) .describe('GPU model fragment, matched loosely: "H100" finds H100_80GB. null for any.') .nullish(), minGpuCount: z .number() .int() .min(1) .max(100_000) .describe('Smallest acceptable GPU count per listing. null for any.') .nullish(), requiresFastInterconnect: z .boolean() .describe('True to keep only Infiniband, RoCE or NVLink — the training-grade fabrics.') .nullish(), }) .strict(), execute: async (input) => listInventory(db, { gpuType: input.gpuType ?? undefined, minGpuCount: input.minGpuCount ?? undefined, requiresFastInterconnect: input.requiresFastInterconnect ?? undefined, }), }), ]; } // --------------------------------------------------------------------------- // Search // --------------------------------------------------------------------------- /** A hit plus the fields ranking needs and the payload does not. */ interface RankedHit { rank: number; name: string; hit: Record; } /** * `%` and `_` are LIKE wildcards and this string arrives from a model. Left * unescaped, a query of `%` matches every row in every table and the model is * handed the first five rows of each as though they were answers. */ export function likeFragment(query: string): string { return `%${query.replace(/[\\%_]/g, (character) => `\\${character}`)}%`; } /** * Exact name first, then prefix, then anything containing the fragment. * * "Meridian" matches two accounts in the demo book — a sovereign customer and a * supply partner — so the tie-break is not academic: an unranked list buries the * exact match the user named behind whichever row the planner returned first. */ function matchRank(name: string, query: string): number { const lowered = name.toLowerCase(); const needle = query.toLowerCase(); if (lowered === needle) return 0; if (lowered.startsWith(needle)) return 1; return 2; } /** * The tie-break when the match quality is identical. * * Searching "Halcyon" in the demo book matches one account and three contracts, * none of them a prefix match, so without this the list opens with a data * processing addendum and the account — the record that reaches the other three * through `pig_get_record_by_id` — is third. The hub record goes first. */ const TYPE_PRIORITY: Record = { account: 0, demand_deal: 1, supply_deal: 2, commitment: 3, contract: 4, }; async function accountNames( db: Database, ids: readonly string[], ): Promise> { const unique = [...new Set(ids)]; if (unique.length === 0) return new Map(); const rows = await db .select({ id: accounts.id, name: accounts.name }) .from(accounts) .where(inArray(accounts.id, unique)); return new Map(rows.map((row) => [row.id, row.name])); } /** * Five bounded name searches, ranked into one list. * * Each table is read one row past its budget so the result can say it was cut * rather than let the model report "two contracts match" over a capped five. * The counts are per type because "which Meridian?" is answered by the shape of * the result set, not by the first row of it. */ async function searchRecords(db: Database, query: string): Promise { const fragment = likeFragment(query); const take = SEARCH_PER_TYPE + 1; const [ accountRows, demandRows, supplyRows, contractRows, commitmentRows, accountsAll, demandAll, supplyAll, contractsAll, commitmentsAll, ] = await Promise.all([ db .select({ id: accounts.id, name: accounts.name, side: accounts.side, customerSegment: accounts.customerSegment, country: accounts.country, }) .from(accounts) .where(and(isNull(accounts.archivedAt), ilike(accounts.name, fragment))) .limit(take), db .select({ id: demandDeals.id, name: demandDeals.name, accountId: demandDeals.accountId, stage: demandDeals.stage, acvCents: demandDeals.acvCents, tcvCents: demandDeals.tcvCents, expectedCloseDate: demandDeals.expectedCloseDate, }) .from(demandDeals) .where(ilike(demandDeals.name, fragment)) .limit(take), db .select({ id: supplyDeals.id, name: supplyDeals.name, accountId: supplyDeals.accountId, stage: supplyDeals.stage, gpuType: supplyDeals.gpuType, gpuCount: supplyDeals.gpuCount, targetCostPerGpuHourCents: supplyDeals.targetCostPerGpuHourCents, }) .from(supplyDeals) .where(ilike(supplyDeals.name, fragment)) .limit(take), db .select({ id: contracts.id, title: contracts.title, accountId: contracts.accountId, contractType: contracts.type, status: contracts.status, side: contracts.side, expiresAt: contracts.expiresAt, valueCents: contracts.valueCents, }) .from(contracts) .where(ilike(contracts.title, fragment)) .limit(take), db .select({ id: capacityCommitments.id, name: capacityCommitments.name, accountId: capacityCommitments.accountId, gpuType: capacityCommitments.gpuType, gpuCount: capacityCommitments.gpuCount, startsAt: capacityCommitments.startsAt, endsAt: capacityCommitments.endsAt, costPerGpuHourCents: capacityCommitments.costPerGpuHourCents, }) .from(capacityCommitments) .where(ilike(capacityCommitments.name, fragment)) .limit(take), // The five denominators. A search that reports only its hits invites // "there are 3 accounts" from a book of twenty-three, and these counts also // make this tool able to answer how many of a thing exist at all. db.select({ value: count() }).from(accounts).where(isNull(accounts.archivedAt)), db.select({ value: count() }).from(demandDeals), db.select({ value: count() }).from(supplyDeals), db.select({ value: count() }).from(contracts), db.select({ value: count() }).from(capacityCommitments), ]); const names = await accountNames(db, [ ...demandRows.map((row) => row.accountId), ...supplyRows.map((row) => row.accountId), ...contractRows.map((row) => row.accountId), ...commitmentRows.map((row) => row.accountId), ]); const rows = (result: readonly { value: number }[]): number => result[0]?.value ?? 0; return assembleSearchResult(query, { accounts: accountRows, demandDeals: demandRows, supplyDeals: supplyRows, contracts: contractRows, commitments: commitmentRows, accountNames: names, totals: { account: rows(accountsAll), demand_deal: rows(demandAll), supply_deal: rows(supplyAll), contract: rows(contractsAll), commitment: rows(commitmentsAll), }, }); } /** The five row sets a search reads, one row past each budget. */ export interface SearchRowSets { accounts: readonly { id: string; name: string; side: string; customerSegment: string | null; country: string | null; }[]; demandDeals: readonly { id: string; name: string; accountId: string; stage: string; acvCents: number | null; tcvCents: number | null; expectedCloseDate: Date | null; }[]; supplyDeals: readonly { id: string; name: string; accountId: string; stage: string; gpuType: string | null; gpuCount: number | null; targetCostPerGpuHourCents: number | null; }[]; contracts: readonly { id: string; title: string; accountId: string; contractType: string; status: string; side: string; expiresAt: Date | null; valueCents: number | null; }[]; commitments: readonly { id: string; name: string; accountId: string; gpuType: string; gpuCount: number; startsAt: Date; endsAt: Date; costPerGpuHourCents: number; }[]; accountNames: ReadonlyMap; /** * How many rows each searched table holds in total — the denominators the * per-type match counts are drawn from. Keyed by the same names the results * carry, so a model reading `counts.account: 1` beside `totals.account: 23` * cannot mistake a name match for a census. */ totals: Record; } /** The five types a name search covers. People are deliberately not indexed. */ export type SearchedRecordType = Exclude; /** * Ranking, capping and counting, with no database in sight. * * Split out from the reads so the two things that can silently go wrong here — * a count taken off a capped list, and an exact match sorted below a * coincidental substring — are pinned by the unit suite. That suite runs in CI * before the migration step, so anything it can reach must not need tables. */ export function assembleSearchResult(query: string, sets: SearchRowSets): unknown { const names = sets.accountNames; const cut = (rows: readonly Row[]): { rows: readonly Row[]; truncated: boolean } => ({ rows: rows.slice(0, SEARCH_PER_TYPE), truncated: rows.length > SEARCH_PER_TYPE, }); const accountsCut = cut(sets.accounts); const demandCut = cut(sets.demandDeals); const supplyCut = cut(sets.supplyDeals); const contractsCut = cut(sets.contracts); const commitmentsCut = cut(sets.commitments); const ranked: RankedHit[] = [ ...accountsCut.rows.map((row) => ({ rank: matchRank(row.name, query), name: row.name, hit: { type: 'account', id: row.id, name: row.name, side: row.side, customerSegment: row.customerSegment, country: row.country, }, })), ...demandCut.rows.map((row) => ({ rank: matchRank(row.name, query), name: row.name, hit: { type: 'demand_deal', id: row.id, name: row.name, accountName: names.get(row.accountId) ?? null, stage: row.stage, acvCents: row.acvCents, tcvCents: row.tcvCents, expectedCloseDate: row.expectedCloseDate?.toISOString() ?? null, }, })), ...supplyCut.rows.map((row) => ({ rank: matchRank(row.name, query), name: row.name, hit: { type: 'supply_deal', id: row.id, name: row.name, accountName: names.get(row.accountId) ?? null, stage: row.stage, gpuType: row.gpuType, gpuCount: row.gpuCount, targetCostPerGpuHourCents: row.targetCostPerGpuHourCents, }, })), ...contractsCut.rows.map((row) => ({ rank: matchRank(row.title, query), name: row.title, hit: { type: 'contract', id: row.id, name: row.title, accountName: names.get(row.accountId) ?? null, contractType: row.contractType, status: row.status, side: row.side, expiresAt: row.expiresAt?.toISOString() ?? null, valueCents: row.valueCents, }, })), ...commitmentsCut.rows.map((row) => ({ rank: matchRank(row.name, query), name: row.name, hit: { type: 'commitment', id: row.id, name: row.name, accountName: names.get(row.accountId) ?? null, gpuType: row.gpuType, gpuCount: row.gpuCount, startsAt: row.startsAt.toISOString(), endsAt: row.endsAt.toISOString(), costPerGpuHourCents: row.costPerGpuHourCents, }, })), ].sort( (a, b) => a.rank - b.rank || (TYPE_PRIORITY[String(a.hit.type)] ?? 9) - (TYPE_PRIORITY[String(b.hit.type)] ?? 9) || a.name.localeCompare(b.name), ); const counts = { account: accountsCut.rows.length, demand_deal: demandCut.rows.length, supply_deal: supplyCut.rows.length, contract: contractsCut.rows.length, commitment: commitmentsCut.rows.length, }; const perTypeTruncated = accountsCut.truncated || demandCut.truncated || supplyCut.truncated || contractsCut.truncated || commitmentsCut.truncated; const results = ranked.slice(0, SEARCH_RESULTS); const truncated = perTypeTruncated || ranked.length > results.length; const searched = Object.values(sets.totals).reduce((sum, rows) => sum + rows, 0); const breakdown = Object.entries(counts) .filter(([, matches]) => matches > 0) .map(([type, matches]) => `${matches} of ${sets.totals[type as SearchedRecordType]} ${type}(s)`) .join(', '); return { headline: results.length === 0 ? `None of the ${searched} account(s), deal(s), contract(s) and capacity commitment(s) ` + `on the book has a name containing "${query}".` : `${truncated ? 'At least ' : ''}${ranked.length} of ${searched} searchable record(s) ` + `match "${query}": ${breakdown}. Those are name matches, not totals; the counts they ` + 'were drawn from are beside them.', scope: resultScope({ covers: `have a name containing "${query}"`, matched: ranked.length, total: searched, totalLabel: 'record(s) searchable by name: accounts, demand deals, supply deals, contracts and capacity commitments', listed: results.length, filters: { query }, truncated, }), query, truncated, counts, /** The denominator for each entry in `counts`, keyed identically. */ totals: sets.totals, results: results.map((entry) => entry.hit), }; } // --------------------------------------------------------------------------- // Renewals // --------------------------------------------------------------------------- /** * Executed paper that has not yet expired, nearest deadline first. * * The deadline is the notice date where one exists, because that — not the * expiry — is the date after which the decision is no longer available. A * contract whose notice window opened last week therefore sorts to the top with * a negative `daysUntilDeadline` and `renewalState: "due"`, which is exactly the * row somebody is looking for when they ask what they have missed. * * `renewalAlarm` is the API's own definition of that arithmetic and is called * rather than repeated: two implementations of expiry-minus-notice would * eventually disagree, and Piggy contradicting the contracts page is worse than * Piggy having no renewals tool. */ async function listRenewals(db: Database, side: 'demand' | 'supply' | undefined): Promise { const now = new Date(); // The denominator is counted on the same side filter the list uses, so // "4 of 20" and "4 of 11 on the demand side" are both answers to the // question that was actually asked. const [rows, all] = await Promise.all([ db .select({ contract: contracts, accountName: accounts.name }) .from(contracts) .leftJoin(accounts, eq(accounts.id, contracts.accountId)) .where( and( eq(contracts.status, 'executed'), isNull(contracts.terminatedAt), isNotNull(contracts.expiresAt), gt(contracts.expiresAt, now), side ? eq(contracts.side, side) : undefined, ), ) .orderBy(asc(contracts.expiresAt)) .limit(SCAN_LIMIT + 1), db .select({ value: count() }) .from(contracts) .where(side ? eq(contracts.side, side) : undefined), ]); return assembleRenewals(rows.slice(0, SCAN_LIMIT), { now, side, truncated: rows.length > SCAN_LIMIT, totalContracts: all[0]?.value ?? 0, }); } /** Exactly the contract columns the renewal projection reads. */ export type RenewalContract = Pick< Contract, 'id' | 'title' | 'side' | 'type' | 'isAutoRenew' | 'noticeDays' | 'expiresAt' | 'valueCents' >; export interface RenewalRow { contract: RenewalContract; accountName: string | null; } /** * The deadline projection, with no database in sight. * * Separated from the read because the two things worth pinning here are the * ordering — a lapsed notice must outrank a distant expiry — and the fact that * `count` is the whole set while `renewals` is a capped slice of it. */ export function assembleRenewals( rows: readonly RenewalRow[], options: { now: Date; side?: 'demand' | 'supply'; truncated: boolean; /** Every contract on this side, whatever its status. The denominator. */ totalContracts: number; }, ): unknown { const { now, side, truncated, totalContracts } = options; const renewals = rows .flatMap(({ contract, accountName }) => { // The query already requires an expiry; narrowing here rather than // asserting keeps the sort key a date the compiler agrees exists. const expiresAt = contract.expiresAt; if (!expiresAt) return []; const alarm = renewalAlarm(contract, now); const deadlineAt = alarm.renewalNoticeAt ?? expiresAt; return [{ id: contract.id, title: contract.title, accountName, side: contract.side, contractType: contract.type, isAutoRenew: contract.isAutoRenew, noticeDays: contract.noticeDays, expiresAt: expiresAt.toISOString(), renewalNoticeAt: alarm.renewalNoticeAt?.toISOString() ?? null, renewalState: alarm.renewalState, deadlineAt: deadlineAt.toISOString(), deadlineKind: alarm.renewalNoticeAt ? ('renewal_notice' as const) : ('expiry' as const), // Negative once the notice window has opened. Read alongside // renewalState rather than on its own. daysUntilDeadline: Math.ceil((deadlineAt.getTime() - now.getTime()) / 86_400_000), valueCents: contract.valueCents, }]; }) .sort((a, b) => a.deadlineAt.localeCompare(b.deadlineAt)); const noticeOpen = renewals.filter((row) => row.renewalState === 'due'); const nearest = renewals[0]; /** * Master agreements routinely carry no `valueCents` — the money sits on the * order forms beneath them. Summing nulls to zero and printing "$0.00 of * stated contract value" reads as a worthless renewal rather than an * unpriced one, so the clause is only stated where a figure exists. */ const statedValueCents = noticeOpen.reduce((sum, row) => sum + (row.valueCents ?? 0), 0); const anyStatedValue = noticeOpen.some((row) => row.valueCents != null); const sideLabel = side ? `${side}-side contract(s) on the book` : 'contract(s) on the book'; const listed = Math.min(renewals.length, EXEMPLARS); return { headline: (nearest ? `${truncated ? 'At least ' : ''}${renewals.length} of ${totalContracts} ${sideLabel} ` + 'are executed and not yet expired' + `${renewals.length > listed ? `; the nearest ${listed} are listed` : ''}. ` + 'Nearest deadline: the ' + `${nearest.deadlineKind === 'renewal_notice' ? 'renewal notice' : 'expiry'} for ` + `${nearest.title}${nearest.accountName ? ` (${nearest.accountName})` : ''} on ` + `${nearest.deadlineAt.slice(0, 10)}` + `${nearest.daysUntilDeadline < 0 ? ', which has already passed' : ''}.` : `None of the ${totalContracts} ${sideLabel} is executed with an expiry date ahead of it.`) + (noticeOpen.length > 0 ? ` ${noticeOpen.length} of those ${renewals.length} have a notice window already open` + (anyStatedValue ? `, covering ${formatCents(statedValueCents)} of stated contract value.` : '; none of those contracts states a value of its own.') : ''), scope: resultScope({ covers: 'are executed, not terminated and not yet expired', matched: renewals.length, total: totalContracts, totalLabel: sideLabel, listed, filters: { side: side ?? 'both', status: 'executed', expired: 'excluded' }, truncated, }), side: side ?? 'both', truncated, count: renewals.length, totalContracts, noticeWindowOpenCount: noticeOpen.length, /** A filter over a filter, so it states its own denominator too. */ noticeWindowOpenScope: resultScope({ covers: 'have a renewal-notice window that is already open', matched: noticeOpen.length, total: renewals.length, totalLabel: `executed, unexpired ${sideLabel}`, listed: 0, filters: { renewalState: 'due' }, truncated, }), renewals: renewals.slice(0, EXEMPLARS), }; } // --------------------------------------------------------------------------- // Provider inventory // --------------------------------------------------------------------------- interface InventoryQuery { gpuType?: string; minGpuCount?: number; requiresFastInterconnect?: boolean; } /** * What providers are offering right now, cheapest first. * * `CapacityService.searchInventory` decides what counts as purchasable — it * drops `Unavailable` stock — and is called rather than re-queried so that * Piggy and the capacity page never disagree about what is on the market. * * Its `gpuType` filter is an exact match, which is wrong for this caller: a * model asked about "H100" sends "H100", and the seeded SKU is `H100_80GB`, so * an exact filter answers "nothing" to a question with eleven answers. The * filter is therefore applied here as a case-insensitive fragment over a wide * bounded read. The width never leaves this process; only EXEMPLARS rows do. */ async function listInventory(db: Database, query: InventoryQuery): Promise { const capacity = new CapacityService(db); // The unfiltered read is the denominator, and it is taken through the same // service rather than counted here: the service decides what "purchasable" // means (it drops Unavailable stock), and a denominator computed from a // second definition of that word would disagree with its own numerator. const [listings, market] = await Promise.all([ capacity.searchInventory({ minGpuCount: query.minGpuCount, requiresHighSpeedInterconnect: query.requiresFastInterconnect, limit: SCAN_LIMIT, }), capacity.searchInventory({ limit: SCAN_LIMIT }), ]); const providerNames = await accountNames( db, listings.flatMap((listing) => (listing.accountId ? [listing.accountId] : [])), ); return assembleInventoryResult(query, listings, { // The service caps at its own ceiling, so a full page is the only signal // available that there was more behind it. truncated: listings.length >= SCAN_LIMIT, providerNames, totalListings: market.length, totalTruncated: market.length >= SCAN_LIMIT, }); } /** Exactly the listing columns the offer projection reads. */ export type InventoryOffer = Pick< InventoryListing, | 'accountId' | 'providerSlug' | 'gpuType' | 'gpuCount' | 'interconnectType' | 'region' | 'country' | 'securityTier' | 'stockStatus' | 'isSpot' | 'onDemandPriceCents' | 'priceIsVariable' | 'observedAt' >; /** * Fragment matching, price ranking and the exemplar cap, with no database in * sight — so the unit suite can pin the ordering that decides which offer a * seller is shown first. */ export function assembleInventoryResult( query: InventoryQuery, listings: readonly InventoryOffer[], options: { truncated: boolean; providerNames: ReadonlyMap; /** Purchasable listings on the market with no filter applied at all. */ totalListings: number; totalTruncated: boolean; }, ): unknown { const { truncated, providerNames: providers, totalListings, totalTruncated } = options; const needle = query.gpuType?.toLowerCase(); const matched = needle ? listings.filter((listing) => listing.gpuType.toLowerCase().includes(needle)) : listings; // Unpriced listings are real — some providers quote on request — but they // cannot be ranked on price, so they sort last rather than as free capacity. const ranked = [...matched].sort( (a, b) => (a.onDemandPriceCents ?? Infinity) - (b.onDemandPriceCents ?? Infinity), ); const cheapest = ranked.find((listing) => listing.onDemandPriceCents != null); const filters = { gpuType: query.gpuType ?? null, minGpuCount: query.minGpuCount ?? null, requiresFastInterconnect: query.requiresFastInterconnect ?? false, }; const anyFilter = Object.values(filters).some((value) => value !== null && value !== false); const listed = Math.min(ranked.length, EXEMPLARS); const market = 'purchasable listing(s) on the market'; return { headline: ranked.length === 0 ? `None of the ${atLeast(totalListings, totalTruncated)} ${market} matches that ` + `request${query.gpuType ? ` for ${query.gpuType}` : ''}.` : `${ranked.length} of ${atLeast(totalListings, totalTruncated)} ${market} match` + `${anyFilter ? ' the filters given' : ' (no filter was applied)'}` + `${query.gpuType ? `, including ${query.gpuType}` : ''}` + (cheapest ? `; cheapest on-demand is ${formatCents(cheapest.onDemandPriceCents ?? 0)} per ` + `GPU-hour for ${cheapest.gpuType}.` : '; none of them carry a published on-demand price.') + ` ${listed} listed here.`, scope: resultScope({ covers: anyFilter ? 'match the filters given' : 'are purchasable', matched: ranked.length, total: totalListings, totalLabel: market, listed, // An unasked-for filter is not a filter: passing the three nulls through // would have an unfiltered result describe itself as a slice. filters: anyFilter ? filters : {}, truncated: truncated || totalTruncated, }), truncated, count: ranked.length, totalListings, filters, listings: ranked.slice(0, EXEMPLARS).map((listing) => shapeListing(listing, providers)), }; } /** * One listing, small enough to quote. * * The price column is stored as `onDemandPriceCents` but is normalised per GPU * on the way in (`packages/prime/src/map.ts`), so it is renamed on the way out. * A model that reads a bare "price" for an eight-GPU node as the node price * quotes a rate eight times too low, and the suffix is what the units rule in * the system prompt keys on. */ function shapeListing( listing: InventoryOffer, providers: ReadonlyMap, ): Record { return { providerName: listing.accountId ? providers.get(listing.accountId) ?? null : null, providerSlug: listing.providerSlug, gpuType: listing.gpuType, gpuCount: listing.gpuCount, interconnectType: listing.interconnectType, region: listing.region, country: listing.country, securityTier: listing.securityTier, stockStatus: listing.stockStatus, isSpot: listing.isSpot, onDemandPricePerGpuHourCents: listing.onDemandPriceCents, priceIsVariable: listing.priceIsVariable, // A listing nobody has confirmed for a week is a quote, not a price. observedAt: listing.observedAt.toISOString(), }; }