Make every tool result say what it counted
Asked how many capacity commitments were on the book, Piggy answered "3". Production holds 5. It had called the idle-capacity tool, which filters to blocks above an idle threshold, and read the length of that list as the size of the book. The system prompt already forbade this in terms — "never report a filtered count as a total; pig_get_idle_capacity returns the blocks with idle hours, not the book" — and the model did it anyway. That is the second time this argument has been lost in the prompt, so it is settled in the payload instead: a result that cannot describe its own scope will be misread eventually, however firmly the prompt objects. Every tool that returns a count or a collection now carries one shape: what it covers, how many matched, out of how many, under which filters, and whether the list was truncated. The denominators are read from the database rather than inferred. The pre-formatted headline states the scope too, since that is the sentence a small model quotes most readily — the idle tool now opens "3 of 5 live capacity commitments on the book", which is the sentence that makes the original mistake impossible to phrase. Two details worth keeping. Record reads enumerate rather than filter, so their scope states a boundary instead of a ratio: these are that record's own figures, never book-wide totals. And the workspace summary's idle threshold is deliberately recorded as 0, distinct from the idle tool's 0.25 — that mismatch is why three different idle figures appeared across the UI, and naming it in the data is how it stops being invisible. Verified against the live model: the failing question now answers 5, demand deals 13 and contracts 20 — each drawn from a payload whose filtered figure was smaller — while "which blocks are sitting idle" still names exactly the blocks that are. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+319
-31
@@ -48,7 +48,7 @@ import {
|
||||
type Database,
|
||||
} from '@pig/db';
|
||||
import { CalendarService } from '@pig/api/src/services/calendar';
|
||||
import { and, gte, inArray, isNull } from 'drizzle-orm';
|
||||
import { and, count, gte, inArray, isNull } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import { piggyPageGuide, type PiggyPageToolName } from './page-routes';
|
||||
import { defineTool, type AgentTool } from './provider';
|
||||
@@ -84,8 +84,107 @@ const TRUNCATION_NOTE =
|
||||
'book — present them as a lower bound, not as the whole.';
|
||||
|
||||
/** Prefixes a count the model must not read as exact. */
|
||||
function atLeast(count: number, truncated: boolean): string {
|
||||
return truncated ? `at least ${count}` : `${count}`;
|
||||
export function atLeast(rows: number, truncated: boolean): string {
|
||||
return truncated ? `at least ${rows}` : `${rows}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* What a result covers, and what it was drawn from.
|
||||
*
|
||||
* Measured in production on /capacity, an hour before this was written. Asked
|
||||
* "How many capacity commitments are on the book?", the model called
|
||||
* `pig_get_idle_capacity` — the only tool that page offers — and answered "3".
|
||||
* The book held 5. The tool filters to blocks at least 25% unsold, so 3 was the
|
||||
* size of a filter; nothing in the payload said so, and reading the length of
|
||||
* the list it had been handed as the size of the book was the only reading the
|
||||
* data supported.
|
||||
*
|
||||
* The system prompt already forbade exactly that, naming this exact tool, and
|
||||
* the model did it anyway. Prompting a 30B model out of a mistake its data
|
||||
* invites does not work, so the data stopped inviting it: every result here
|
||||
* that carries a count or a collection carries this object beside it, naming
|
||||
* the filter, the denominator it was drawn from, and how much of the matched
|
||||
* set is actually listed. A filtered count is therefore never the only number
|
||||
* in its own result.
|
||||
*
|
||||
* One shape to learn rather than one per tool — a different shape per tool is
|
||||
* how this happened. The result's primary subject gets a top-level `scope`;
|
||||
* every other count or collection in the same payload gets its own, nested
|
||||
* where the payload already groups it and named `<field>Scope` where it does
|
||||
* not. And `summary` restates the numbers as prose on purpose — it is the
|
||||
* field a small model quotes, and a figure it has to assemble out of three
|
||||
* other fields is a figure it will assemble wrongly.
|
||||
*
|
||||
* `filters` is not decoration either. This product has shipped three different
|
||||
* idle figures across three surfaces because each applied its own threshold, so
|
||||
* a result that does not name the threshold it used cannot be reconciled with
|
||||
* the screen beside it.
|
||||
*/
|
||||
export interface ResultScope {
|
||||
/** The whole scope in one sentence, figures included. Quote this. */
|
||||
summary: string;
|
||||
/** What made a row match, as a clause: "are at least 25% unsold". */
|
||||
covers: string;
|
||||
/** How many rows matched. Never the answer to "how many are there". */
|
||||
matched: number;
|
||||
/** The set `matched` was drawn from. This is the total. */
|
||||
total: number;
|
||||
/** What `total` counts, as a noun phrase. */
|
||||
totalLabel: string;
|
||||
/** How many of `matched` this payload lists. The rest are counted only. */
|
||||
listed: number;
|
||||
/** Every filter applied, named, so a threshold is never invisible. */
|
||||
filters: Record<string, string | number | boolean | null>;
|
||||
/** True when a read hit its row cap, so both figures are lower bounds. */
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
export function resultScope(input: {
|
||||
covers: string;
|
||||
matched: number;
|
||||
total: number;
|
||||
totalLabel: string;
|
||||
listed: number;
|
||||
filters?: Record<string, string | number | boolean | null>;
|
||||
truncated?: boolean;
|
||||
}): ResultScope {
|
||||
const { covers, matched, total, totalLabel, listed } = input;
|
||||
const filters = input.filters ?? {};
|
||||
const truncated = input.truncated ?? false;
|
||||
// Nothing was filtered out, so `matched` IS the total and saying otherwise
|
||||
// would teach the model to distrust a figure that is exact.
|
||||
const unfiltered = matched === total && Object.keys(filters).length === 0;
|
||||
const listedClause = listed > 0 ? `; ${listed} listed here` : '';
|
||||
// Both figures are hedged together when a read was cut. Hedging only the
|
||||
// total would present a capped `matched` as exact, which is the same class of
|
||||
// overstatement this whole object exists to stop.
|
||||
const summary = unfiltered
|
||||
? `All ${atLeast(total, truncated)} ${totalLabel}${listedClause}.`
|
||||
: `${atLeast(matched, truncated)} of ${atLeast(total, truncated)} ${totalLabel} ` +
|
||||
`${covers}${listedClause}. That is a filtered count — the total is ` +
|
||||
`${atLeast(total, truncated)} ${totalLabel}.`;
|
||||
return {
|
||||
summary: truncated ? `${summary} ${TRUNCATION_NOTE}` : summary,
|
||||
covers,
|
||||
matched,
|
||||
total,
|
||||
totalLabel,
|
||||
listed,
|
||||
filters,
|
||||
truncated,
|
||||
};
|
||||
}
|
||||
|
||||
/** The denominator every commitment figure in this file is drawn from. */
|
||||
const COMMITMENTS_LABEL = 'live capacity commitment(s) on the book';
|
||||
|
||||
/** The denominators the two pipelines are drawn from. */
|
||||
const DEMAND_DEALS_LABEL = 'demand deal(s) on the book';
|
||||
const SUPPLY_DEALS_LABEL = 'supply deal(s) on the book';
|
||||
|
||||
/** One whole-table count, for use as a denominator. */
|
||||
function rowCount(rows: readonly { value: number }[]): number {
|
||||
return rows[0]?.value ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -306,9 +405,24 @@ async function readMarginSummary(db: Database): Promise<unknown> {
|
||||
headline:
|
||||
`Revenue ${formatCents(totals.revenueCents)} against cost ${formatCents(totals.costCents)}; ` +
|
||||
`gross margin ${formatCents(totals.grossMarginCents)} (${percent(totals.grossMarginPct)}) ` +
|
||||
`at ${percent(totals.utilisation)} utilisation across ` +
|
||||
`${atLeast(blocks.length, truncated)} live commitment(s).` +
|
||||
`at ${percent(totals.utilisation)} utilisation across all ` +
|
||||
`${atLeast(blocks.length, truncated)} ${COMMITMENTS_LABEL} — the whole book, unfiltered. ` +
|
||||
`The ${largest.length} largest by cost are listed; the book holds ` +
|
||||
`${atLeast(blocks.length, truncated)}.` +
|
||||
(truncated ? ` ${TRUNCATION_NOTE}` : ''),
|
||||
/**
|
||||
* Unfiltered, and the only tool here that is: `matched` equals `total`, so
|
||||
* `liveCommitments` below is a real answer to "how many are on the book".
|
||||
* `largestBlocks` is still a slice, which is what `listed` is for.
|
||||
*/
|
||||
scope: resultScope({
|
||||
covers: 'are live',
|
||||
matched: blocks.length,
|
||||
total: blocks.length,
|
||||
totalLabel: COMMITMENTS_LABEL,
|
||||
listed: largest.length,
|
||||
truncated,
|
||||
}),
|
||||
truncated,
|
||||
totals: {
|
||||
revenueCents: totals.revenueCents,
|
||||
@@ -332,13 +446,27 @@ async function readMarginSummary(db: Database): Promise<unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The threshold and horizon this tool filters on.
|
||||
*
|
||||
* The same defaults the API and MCP use — and NOT the same as the workspace
|
||||
* summary's worst-idle list, which takes any block with idle hours at all.
|
||||
* Both are correct for what they answer and they return different counts, so
|
||||
* each states its own threshold in `filters` rather than leaving the reader to
|
||||
* reconcile two figures that were never the same figure.
|
||||
*/
|
||||
const IDLE_THRESHOLD_PCT = 0.25;
|
||||
const IDLE_WITHIN_DAYS = 30;
|
||||
|
||||
/** Idle blocks, on the same defaults the API and MCP use: 25% within 30 days. */
|
||||
async function readIdleCapacity(db: Database): Promise<unknown> {
|
||||
const now = new Date();
|
||||
const horizon = new Date(now.getTime() + 30 * 86_400_000);
|
||||
const horizon = new Date(now.getTime() + IDLE_WITHIN_DAYS * 86_400_000);
|
||||
const { blocks, truncated } = await readLiveBlocks(db, now);
|
||||
const idle = blocks
|
||||
.filter((block) => block.startsAt <= horizon && 1 - block.margin.utilisation >= 0.25)
|
||||
.filter(
|
||||
(block) => block.startsAt <= horizon && 1 - block.margin.utilisation >= IDLE_THRESHOLD_PCT,
|
||||
)
|
||||
.map((block) => ({
|
||||
block,
|
||||
idleGpuHours: block.margin.idleGpuHours,
|
||||
@@ -348,19 +476,42 @@ async function readIdleCapacity(db: Database): Promise<unknown> {
|
||||
.sort((a, b) => b.idleCostCents - a.idleCostCents);
|
||||
|
||||
const totalIdleCostCents = idle.reduce((sum, row) => sum + row.idleCostCents, 0);
|
||||
const listed = idle.slice(0, EXEMPLARS);
|
||||
const filter =
|
||||
`are at least ${IDLE_THRESHOLD_PCT * 100}% unsold and start within ` +
|
||||
`${IDLE_WITHIN_DAYS} day(s)`;
|
||||
|
||||
return {
|
||||
/**
|
||||
* The sentence the production defect was answered from, so it carries the
|
||||
* denominator first and the filtered figure second. "3" alone was true of
|
||||
* the filter and false of the book; "3 of 5" cannot be misread as 5.
|
||||
*/
|
||||
headline:
|
||||
(idle.length === 0
|
||||
? 'No live block is more than 25% unsold within the next 30 days.'
|
||||
: `${atLeast(idle.length, truncated)} block(s) at least 25% unsold within 30 days, ` +
|
||||
`${formatCents(totalIdleCostCents)} of capacity bought and not yet earning.`) +
|
||||
? `None of the ${atLeast(blocks.length, truncated)} ${COMMITMENTS_LABEL} ${filter}.`
|
||||
: `${idle.length} of ${atLeast(blocks.length, truncated)} ${COMMITMENTS_LABEL} ${filter}, ` +
|
||||
`${formatCents(totalIdleCostCents)} of capacity bought and not yet earning. ` +
|
||||
`${idle.length} is a filtered count — the book holds ` +
|
||||
`${atLeast(blocks.length, truncated)} live commitment(s) in total.`) +
|
||||
(truncated ? ` ${TRUNCATION_NOTE}` : ''),
|
||||
scope: resultScope({
|
||||
covers: filter,
|
||||
matched: idle.length,
|
||||
total: blocks.length,
|
||||
totalLabel: COMMITMENTS_LABEL,
|
||||
listed: listed.length,
|
||||
filters: { idleThresholdPct: IDLE_THRESHOLD_PCT, withinDays: IDLE_WITHIN_DAYS },
|
||||
truncated,
|
||||
}),
|
||||
truncated,
|
||||
thresholdPct: 0.25,
|
||||
withinDays: 30,
|
||||
/** The denominator, repeated as a bare field: this is the size of the book. */
|
||||
liveCommitments: blocks.length,
|
||||
thresholdPct: IDLE_THRESHOLD_PCT,
|
||||
withinDays: IDLE_WITHIN_DAYS,
|
||||
idleBlocks: idle.length,
|
||||
totalIdleCostCents,
|
||||
blocks: idle.slice(0, EXEMPLARS).map((row) => ({
|
||||
blocks: listed.map((row) => ({
|
||||
name: row.block.name,
|
||||
gpuType: row.block.gpuType,
|
||||
gpuCount: row.block.gpuCount,
|
||||
@@ -379,7 +530,10 @@ async function readIdleCapacity(db: Database): Promise<unknown> {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function readPipeline(db: Database): Promise<unknown> {
|
||||
const [demandRead, supplyRead] = await Promise.all([
|
||||
// The two whole-table counts are the denominators. Without them "12 open
|
||||
// demand deals" is a filtered count with nothing to be filtered from, and
|
||||
// "how many deals do we have" is answered with the number of open ones.
|
||||
const [demandRead, supplyRead, demandAll, supplyAll] = await Promise.all([
|
||||
db
|
||||
.select()
|
||||
.from(demandDeals)
|
||||
@@ -390,10 +544,17 @@ async function readPipeline(db: Database): Promise<unknown> {
|
||||
.from(supplyDeals)
|
||||
.where(inArray(supplyDeals.stage, [...SUPPLY_OPEN_STAGES]))
|
||||
.limit(SCAN_LIMIT + 1),
|
||||
db.select({ value: count() }).from(demandDeals),
|
||||
db.select({ value: count() }).from(supplyDeals),
|
||||
]);
|
||||
const { rows: demand, truncated: demandTruncated } = bounded(demandRead);
|
||||
const { rows: supply, truncated: supplyTruncated } = bounded(supplyRead);
|
||||
const truncated = demandTruncated || supplyTruncated;
|
||||
const demandTotal = rowCount(demandAll);
|
||||
const supplyTotal = rowCount(supplyAll);
|
||||
const demandListed = Math.min(demand.length, EXEMPLARS);
|
||||
const supplyListed = Math.min(supply.length, EXEMPLARS);
|
||||
const openStages = 'are at an open stage (not closed, not lost)';
|
||||
|
||||
// Total contract value where it is known, annual value otherwise: a deal
|
||||
// valued only by ACV is still worth counting, and treating it as zero would
|
||||
@@ -403,13 +564,35 @@ async function readPipeline(db: Database): Promise<unknown> {
|
||||
|
||||
return {
|
||||
headline:
|
||||
`${atLeast(demand.length, demandTruncated)} open demand deal(s) worth ` +
|
||||
`${formatCents(demandValueCents)} and ${atLeast(supply.length, supplyTruncated)} ` +
|
||||
'open supply deal(s).' +
|
||||
`${demand.length} of ${atLeast(demandTotal, demandTruncated)} ${DEMAND_DEALS_LABEL} are open, ` +
|
||||
`worth ${formatCents(demandValueCents)}, and ${supply.length} of ` +
|
||||
`${atLeast(supplyTotal, supplyTruncated)} ${SUPPLY_DEALS_LABEL} are open — ` +
|
||||
`${demand.length + supply.length} of ${atLeast(demandTotal + supplyTotal, truncated)} ` +
|
||||
'deal(s) on the book in all. Those are open-stage counts, not the size of either pipeline.' +
|
||||
(truncated ? ` ${TRUNCATION_NOTE}` : ''),
|
||||
// Both sides together, so a question about "deals" has a denominator too.
|
||||
scope: resultScope({
|
||||
covers: openStages,
|
||||
matched: demand.length + supply.length,
|
||||
total: demandTotal + supplyTotal,
|
||||
totalLabel: 'deal(s) on the book, demand and supply together',
|
||||
listed: demandListed + supplyListed,
|
||||
filters: { stages: 'open only' },
|
||||
truncated,
|
||||
}),
|
||||
truncated: { demandDeals: demandTruncated, supplyDeals: supplyTruncated },
|
||||
demand: {
|
||||
scope: resultScope({
|
||||
covers: openStages,
|
||||
matched: demand.length,
|
||||
total: demandTotal,
|
||||
totalLabel: DEMAND_DEALS_LABEL,
|
||||
listed: demandListed,
|
||||
filters: { stages: [...DEMAND_OPEN_STAGES].join(', ') },
|
||||
truncated: demandTruncated,
|
||||
}),
|
||||
openDeals: demand.length,
|
||||
totalDeals: demandTotal,
|
||||
valueCents: demandValueCents,
|
||||
byStage: countByStage(demand.map((deal) => deal.stage)),
|
||||
largest: [...demand]
|
||||
@@ -423,7 +606,17 @@ async function readPipeline(db: Database): Promise<unknown> {
|
||||
})),
|
||||
},
|
||||
supply: {
|
||||
scope: resultScope({
|
||||
covers: openStages,
|
||||
matched: supply.length,
|
||||
total: supplyTotal,
|
||||
totalLabel: SUPPLY_DEALS_LABEL,
|
||||
listed: supplyListed,
|
||||
filters: { stages: [...SUPPLY_OPEN_STAGES].join(', ') },
|
||||
truncated: supplyTruncated,
|
||||
}),
|
||||
openDeals: supply.length,
|
||||
totalDeals: supplyTotal,
|
||||
byStage: countByStage(supply.map((deal) => deal.stage)),
|
||||
largest: [...supply]
|
||||
.sort((a, b) => (b.gpuCount ?? 0) - (a.gpuCount ?? 0))
|
||||
@@ -510,10 +703,22 @@ async function readCalendarAhead(db: Database, withinDays: number): Promise<unkn
|
||||
const overdue = behind.events.filter((event) => event.state === 'overdue');
|
||||
const truncated = ahead.truncated || behind.truncated;
|
||||
const upcomingByKind = countByKind(upcoming);
|
||||
const upcomingListed = Math.min(upcoming.length, EXEMPLARS * 2);
|
||||
const overdueListed = Math.min(overdue.length, EXEMPLARS);
|
||||
/**
|
||||
* Both denominators are windows, not the book, and the labels say so. Nothing
|
||||
* here can answer "how many obligations are there" — only how many fall in
|
||||
* these dates — so a label that read "obligations on the book" would be the
|
||||
* same lie in a different tool.
|
||||
*/
|
||||
const windowLabel = `dated item(s) falling in the next ${withinDays} day(s), done or not`;
|
||||
const lookbackLabel = `dated item(s) in the last ${OVERDUE_LOOKBACK_DAYS} day(s) that can fall late`;
|
||||
|
||||
return {
|
||||
headline:
|
||||
`Next ${withinDays} day(s): ${atLeast(upcoming.length, ahead.truncated)} dated item(s) ` +
|
||||
`Next ${withinDays} day(s) only, not the whole book: ` +
|
||||
`${atLeast(upcoming.length, ahead.truncated)} of ` +
|
||||
`${atLeast(ahead.events.length, ahead.truncated)} dated item(s) ` +
|
||||
`across ${Object.keys(upcomingByKind).length} kind(s), of which ` +
|
||||
`${ahead.totals.obligationCount} obligation(s) due, ${ahead.totals.closingCount} demand ` +
|
||||
`deal(s) expected to close worth ${formatCents(ahead.totals.weightedPipelineCents)} ` +
|
||||
@@ -522,6 +727,15 @@ async function readCalendarAhead(db: Database, withinDays: number): Promise<unkn
|
||||
`${atLeast(overdue.length, behind.truncated)} item(s) overdue in the last ` +
|
||||
`${OVERDUE_LOOKBACK_DAYS} day(s).` +
|
||||
(truncated ? ` ${TRUNCATION_NOTE}` : ''),
|
||||
scope: resultScope({
|
||||
covers: 'are still outstanding',
|
||||
matched: upcoming.length,
|
||||
total: ahead.events.length,
|
||||
totalLabel: windowLabel,
|
||||
listed: upcomingListed,
|
||||
filters: { withinDays, state: 'excludes done' },
|
||||
truncated: ahead.truncated,
|
||||
}),
|
||||
withinDays,
|
||||
truncated,
|
||||
/**
|
||||
@@ -536,8 +750,11 @@ async function readCalendarAhead(db: Database, withinDays: number): Promise<unkn
|
||||
renewalNotices: ahead.totals.renewalCount,
|
||||
expiringExportAuthorizations: ahead.totals.expiringAuthorizationCount,
|
||||
},
|
||||
// The top-level `scope` is this list's: upcoming work is what the tool is
|
||||
// for, and a second copy of the same eight fields is eight fields of budget.
|
||||
upcoming: {
|
||||
count: upcoming.length,
|
||||
inWindow: ahead.events.length,
|
||||
truncated: ahead.truncated,
|
||||
byKind: upcomingByKind,
|
||||
byState: countByState(upcoming),
|
||||
@@ -545,6 +762,18 @@ async function readCalendarAhead(db: Database, withinDays: number): Promise<unkn
|
||||
events: upcoming.slice(0, EXEMPLARS * 2).map(exemplar),
|
||||
},
|
||||
overdue: {
|
||||
scope: resultScope({
|
||||
covers: 'have lapsed without being completed',
|
||||
matched: overdue.length,
|
||||
total: behind.events.length,
|
||||
totalLabel: lookbackLabel,
|
||||
listed: overdueListed,
|
||||
filters: {
|
||||
lookbackDays: OVERDUE_LOOKBACK_DAYS,
|
||||
kinds: [...OVERDUE_KINDS].join(', '),
|
||||
},
|
||||
truncated: behind.truncated,
|
||||
}),
|
||||
count: overdue.length,
|
||||
truncated: behind.truncated,
|
||||
lookbackDays: OVERDUE_LOOKBACK_DAYS,
|
||||
@@ -597,7 +826,7 @@ function countByState(events: readonly CalendarEvent[]): Record<string, number>
|
||||
* answered from the fragment it happened to receive.
|
||||
*/
|
||||
async function readWorkspaceSummary(db: Database): Promise<unknown> {
|
||||
const [book, demandRead, supplyRead] = await Promise.all([
|
||||
const [book, demandRead, supplyRead, demandAll, supplyAll] = await Promise.all([
|
||||
readLiveBlocks(db),
|
||||
db
|
||||
.select({ id: demandDeals.id })
|
||||
@@ -609,8 +838,12 @@ async function readWorkspaceSummary(db: Database): Promise<unknown> {
|
||||
.from(supplyDeals)
|
||||
.where(inArray(supplyDeals.stage, [...SUPPLY_OPEN_STAGES]))
|
||||
.limit(SCAN_LIMIT + 1),
|
||||
db.select({ value: count() }).from(demandDeals),
|
||||
db.select({ value: count() }).from(supplyDeals),
|
||||
]);
|
||||
const { blocks } = book;
|
||||
const demandTotal = rowCount(demandAll);
|
||||
const supplyTotal = rowCount(supplyAll);
|
||||
const { rows: demand, truncated: demandTruncated } = bounded(demandRead);
|
||||
const { rows: supply, truncated: supplyTruncated } = bounded(supplyRead);
|
||||
const truncated = {
|
||||
@@ -620,23 +853,42 @@ async function readWorkspaceSummary(db: Database): Promise<unknown> {
|
||||
};
|
||||
const anyTruncated = Object.values(truncated).some(Boolean);
|
||||
const totals = bookTotals(blocks);
|
||||
const worstIdle = [...blocks]
|
||||
/**
|
||||
* Any idle at all, which is a THIRD threshold — `pig_get_idle_capacity` uses
|
||||
* 25% and the web uses its own. Three surfaces have quoted three different
|
||||
* idle counts for one book because of exactly this, so the scope below names
|
||||
* the threshold rather than leaving the reader to guess which one produced
|
||||
* the number in front of them.
|
||||
*/
|
||||
const withIdle = [...blocks]
|
||||
.filter((block) => block.margin.idleGpuHours > 0)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
b.margin.idleGpuHours * b.costPerGpuHourCents -
|
||||
a.margin.idleGpuHours * a.costPerGpuHourCents,
|
||||
)
|
||||
.slice(0, 3);
|
||||
);
|
||||
const worstIdle = withIdle.slice(0, 3);
|
||||
|
||||
return {
|
||||
headline:
|
||||
`${atLeast(blocks.length, truncated.commitments)} live commitment(s) at ` +
|
||||
`All ${atLeast(blocks.length, truncated.commitments)} ${COMMITMENTS_LABEL} at ` +
|
||||
`${percent(totals.utilisation)} utilisation; ` +
|
||||
`gross margin ${formatCents(totals.grossMarginCents)}; ` +
|
||||
`${atLeast(demand.length, demandTruncated)} open demand and ` +
|
||||
`${atLeast(supply.length, supplyTruncated)} open supply deal(s).` +
|
||||
`${demand.length} of ${atLeast(demandTotal, demandTruncated)} ${DEMAND_DEALS_LABEL} ` +
|
||||
`and ${supply.length} of ${atLeast(supplyTotal, supplyTruncated)} ${SUPPLY_DEALS_LABEL} ` +
|
||||
`are open. The ${worstIdle.length} block(s) listed below are the worst idle of ` +
|
||||
`${withIdle.length} with any idle hours, not the whole book.` +
|
||||
(anyTruncated ? ` ${TRUNCATION_NOTE}` : ''),
|
||||
// The book, unfiltered: `matched` equals `total`, so this is the figure to
|
||||
// quote when someone asks how large the book is.
|
||||
scope: resultScope({
|
||||
covers: 'are live',
|
||||
matched: blocks.length,
|
||||
total: blocks.length,
|
||||
totalLabel: COMMITMENTS_LABEL,
|
||||
listed: 0,
|
||||
truncated: truncated.commitments,
|
||||
}),
|
||||
truncated,
|
||||
book: {
|
||||
liveCommitments: blocks.length,
|
||||
@@ -646,14 +898,50 @@ async function readWorkspaceSummary(db: Database): Promise<unknown> {
|
||||
utilisation: totals.utilisation,
|
||||
idleGpuHours: Math.round(totals.idleGpuHours),
|
||||
},
|
||||
// Two filtered counts, each next to the denominator it came from. Without
|
||||
// `totalDemandDeals` beside it, `openDemandDeals` is the only deal figure
|
||||
// in the payload and becomes the answer to "how many deals do we have".
|
||||
openDemandDeals: demand.length,
|
||||
totalDemandDeals: demandTotal,
|
||||
openDemandDealsScope: resultScope({
|
||||
covers: 'are at an open stage (not closed, not lost)',
|
||||
matched: demand.length,
|
||||
total: demandTotal,
|
||||
totalLabel: DEMAND_DEALS_LABEL,
|
||||
listed: 0,
|
||||
filters: { stages: [...DEMAND_OPEN_STAGES].join(', ') },
|
||||
truncated: demandTruncated,
|
||||
}),
|
||||
openSupplyDeals: supply.length,
|
||||
worstIdleBlocks: worstIdle.map((block) => ({
|
||||
name: block.name,
|
||||
gpuType: block.gpuType,
|
||||
idleGpuHours: Math.round(block.margin.idleGpuHours),
|
||||
idleCostCents: Math.round(block.margin.idleGpuHours * block.costPerGpuHourCents),
|
||||
})),
|
||||
totalSupplyDeals: supplyTotal,
|
||||
openSupplyDealsScope: resultScope({
|
||||
covers: 'are at an open stage (not closed, not lost)',
|
||||
matched: supply.length,
|
||||
total: supplyTotal,
|
||||
totalLabel: SUPPLY_DEALS_LABEL,
|
||||
listed: 0,
|
||||
filters: { stages: [...SUPPLY_OPEN_STAGES].join(', ') },
|
||||
truncated: supplyTruncated,
|
||||
}),
|
||||
worstIdle: {
|
||||
scope: resultScope({
|
||||
covers: 'have any unsold hours at all',
|
||||
matched: withIdle.length,
|
||||
total: blocks.length,
|
||||
totalLabel: COMMITMENTS_LABEL,
|
||||
listed: worstIdle.length,
|
||||
// Not 0.25. This list and pig_get_idle_capacity answer different
|
||||
// questions and will disagree; the thresholds say which is which.
|
||||
filters: { idleThresholdPct: 0 },
|
||||
truncated: truncated.commitments,
|
||||
}),
|
||||
blocks: worstIdle.map((block) => ({
|
||||
name: block.name,
|
||||
gpuType: block.gpuType,
|
||||
idleGpuHours: Math.round(block.margin.idleGpuHours),
|
||||
idleCostCents: Math.round(block.margin.idleGpuHours * block.costPerGpuHourCents),
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user