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:
@@ -20,6 +20,7 @@ import test from 'node:test';
|
||||
import type { Database } from '@pig/db';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
import { assertPigToolBoundary } from '../src/chat';
|
||||
import type { ResultScope } from '../src/page-tools';
|
||||
import {
|
||||
assembleInventoryResult,
|
||||
assembleRenewals,
|
||||
@@ -184,6 +185,24 @@ test('every parameter description survives into the emitted schema', () => {
|
||||
// Search shaping
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The five denominators, roughly the demo book's own shape.
|
||||
*
|
||||
* A search reports how many rows it matched; without these it would be the only
|
||||
* count in its own payload, and "3 accounts match" is one careless sentence away
|
||||
* from "we have 3 accounts".
|
||||
*/
|
||||
const TOTALS = {
|
||||
account: 23,
|
||||
demand_deal: 13,
|
||||
supply_deal: 8,
|
||||
contract: 20,
|
||||
commitment: 6,
|
||||
} as const;
|
||||
|
||||
/** Every row in every searched table: the denominator the headline quotes. */
|
||||
const SEARCHABLE = Object.values(TOTALS).reduce((sum, rows) => sum + rows, 0);
|
||||
|
||||
const emptySets: SearchRowSets = {
|
||||
accounts: [],
|
||||
demandDeals: [],
|
||||
@@ -191,6 +210,7 @@ const emptySets: SearchRowSets = {
|
||||
contracts: [],
|
||||
commitments: [],
|
||||
accountNames: new Map(),
|
||||
totals: { ...TOTALS },
|
||||
};
|
||||
|
||||
function account(name: string, id = name): SearchRowSets['accounts'][number] {
|
||||
@@ -201,6 +221,8 @@ interface SearchReading {
|
||||
headline: string;
|
||||
truncated: boolean;
|
||||
counts: Record<string, number>;
|
||||
totals: Record<string, number>;
|
||||
scope: ResultScope;
|
||||
results: { type: string; id: string; name: string }[];
|
||||
}
|
||||
|
||||
@@ -256,13 +278,19 @@ test('a search result is capped per type and overall, and says when it was cut',
|
||||
assert.equal(reading.truncated, true);
|
||||
// The model quotes the headline, so the hedge has to live in it rather than
|
||||
// in a `truncated` flag further down the payload.
|
||||
assert.match(reading.headline, /at least 5 record\(s\) match "alpha"/);
|
||||
assert.match(reading.headline, new RegExp(`At least 5 of ${SEARCHABLE} searchable record\\(s\\)`));
|
||||
// The denominator travels with the hedge: a capped match count next to the
|
||||
// number of rows it was drawn from cannot be read as "we have five accounts".
|
||||
assert.equal(reading.scope.matched, 5);
|
||||
assert.equal(reading.scope.total, SEARCHABLE);
|
||||
assert.equal(reading.totals.account, TOTALS.account);
|
||||
});
|
||||
|
||||
test('the overall cap holds even when no single type reached its own', () => {
|
||||
const three = (prefix: string) =>
|
||||
Array.from({ length: 3 }, (_, i) => `${prefix} ${i}`);
|
||||
const reading = assembleSearchResult('block', {
|
||||
totals: { ...TOTALS },
|
||||
accounts: three('block acct').map((name) => account(name, name)),
|
||||
demandDeals: three('block demand').map((name) => ({
|
||||
id: name,
|
||||
@@ -355,7 +383,10 @@ test('a search that matches nothing says so rather than returning a bare empty l
|
||||
const reading = assembleSearchResult('nobody', emptySets) as SearchReading;
|
||||
assert.equal(reading.results.length, 0);
|
||||
assert.equal(reading.truncated, false);
|
||||
assert.match(reading.headline, /No account, deal, contract or capacity commitment/);
|
||||
assert.match(reading.headline, new RegExp(`None of the ${SEARCHABLE} account\\(s\\)`));
|
||||
// Even an empty search states the size of what it looked through.
|
||||
assert.equal(reading.scope.matched, 0);
|
||||
assert.equal(reading.scope.total, SEARCHABLE);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -378,10 +409,15 @@ function contract(overrides: Partial<RenewalContract> & { id: string }): Renewal
|
||||
};
|
||||
}
|
||||
|
||||
/** Contracts of every status on the book — the renewal list's denominator. */
|
||||
const CONTRACTS_ON_BOOK = 20;
|
||||
|
||||
interface RenewalReading {
|
||||
headline: string;
|
||||
truncated: boolean;
|
||||
scope: ResultScope;
|
||||
count: number;
|
||||
totalContracts: number;
|
||||
noticeWindowOpenCount: number;
|
||||
renewals: {
|
||||
id: string;
|
||||
@@ -409,7 +445,7 @@ test('a lapsed notice outranks a nearer expiry, because the decision is the dead
|
||||
accountName: 'Halcyon',
|
||||
},
|
||||
],
|
||||
{ now: NOW, truncated: false },
|
||||
{ now: NOW, truncated: false, totalContracts: CONTRACTS_ON_BOOK },
|
||||
) as RenewalReading;
|
||||
|
||||
assert.deepEqual(reading.renewals.map((row) => row.id), ['missed', 'soon']);
|
||||
@@ -441,7 +477,7 @@ test('an open notice window on unpriced paper is not reported as worth nothing',
|
||||
accountName: 'Halcyon',
|
||||
},
|
||||
],
|
||||
{ now: NOW, truncated: false },
|
||||
{ now: NOW, truncated: false, totalContracts: CONTRACTS_ON_BOOK },
|
||||
) as RenewalReading;
|
||||
|
||||
assert.equal(reading.noticeWindowOpenCount, 1);
|
||||
@@ -452,7 +488,7 @@ test('an open notice window on unpriced paper is not reported as worth nothing',
|
||||
test('a contract that cannot auto-renew has an expiry deadline and no notice state', () => {
|
||||
const reading = assembleRenewals(
|
||||
[{ contract: contract({ id: 'plain' }), accountName: 'Verity Health AI' }],
|
||||
{ now: NOW, truncated: false },
|
||||
{ now: NOW, truncated: false, totalContracts: CONTRACTS_ON_BOOK },
|
||||
) as RenewalReading;
|
||||
|
||||
const [row] = reading.renewals;
|
||||
@@ -468,20 +504,25 @@ test('the renewal count covers the whole set while the list is capped', () => {
|
||||
contract: contract({ id: `c${i}`, expiresAt: new Date(NOW.getTime() + (i + 1) * DAY) }),
|
||||
accountName: null,
|
||||
}));
|
||||
const reading = assembleRenewals(rows, { now: NOW, truncated: true }) as RenewalReading;
|
||||
const reading = assembleRenewals(rows, { now: NOW, truncated: true, totalContracts: CONTRACTS_ON_BOOK }) as RenewalReading;
|
||||
|
||||
assert.equal(reading.count, 14);
|
||||
assert.equal(reading.renewals.length, 8);
|
||||
assert.equal(reading.truncated, true);
|
||||
// A capped list quoted as a total is the defect this whole pattern exists to
|
||||
// prevent, so the hedge has to reach the headline.
|
||||
assert.match(reading.headline, /At least 14 executed contract\(s\)/);
|
||||
assert.match(
|
||||
reading.headline,
|
||||
new RegExp(`At least 14 of ${CONTRACTS_ON_BOOK} contract\\(s\\) on the book are executed`),
|
||||
);
|
||||
assert.equal(reading.scope.matched, 14);
|
||||
assert.equal(reading.scope.total, CONTRACTS_ON_BOOK);
|
||||
});
|
||||
|
||||
test('an empty book states the absence rather than implying nothing is due', () => {
|
||||
const reading = assembleRenewals([], { now: NOW, side: 'supply', truncated: false }) as RenewalReading;
|
||||
const reading = assembleRenewals([], { now: NOW, side: 'supply', truncated: false, totalContracts: 8 }) as RenewalReading;
|
||||
assert.equal(reading.count, 0);
|
||||
assert.match(reading.headline, /No executed contract on the supply side/);
|
||||
assert.match(reading.headline, /None of the 8 supply-side contract\(s\) on the book/);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -508,10 +549,15 @@ function offer(overrides: Partial<InventoryOffer> & { gpuType: string }): Invent
|
||||
|
||||
const providerNames = new Map([['provider-1', 'RunPod']]);
|
||||
|
||||
/** Purchasable listings on the market with no filter at all: the denominator. */
|
||||
const LISTINGS_ON_MARKET = 30;
|
||||
|
||||
interface InventoryReading {
|
||||
headline: string;
|
||||
truncated: boolean;
|
||||
scope: ResultScope;
|
||||
count: number;
|
||||
totalListings: number;
|
||||
listings: {
|
||||
gpuType: string;
|
||||
providerName: string | null;
|
||||
@@ -527,7 +573,7 @@ test('offers are cheapest first, and an unpriced one sorts last rather than free
|
||||
offer({ gpuType: 'QUOTE_ONLY', onDemandPriceCents: null }),
|
||||
offer({ gpuType: 'H100_80GB', onDemandPriceCents: 189 }),
|
||||
],
|
||||
{ truncated: false, providerNames },
|
||||
{ truncated: false, providerNames, totalListings: LISTINGS_ON_MARKET, totalTruncated: false },
|
||||
) as InventoryReading;
|
||||
|
||||
assert.deepEqual(reading.listings.map((row) => row.gpuType), [
|
||||
@@ -546,7 +592,7 @@ test('a GPU-type fragment matches the SKU, because a model asks for H100', () =>
|
||||
const reading = assembleInventoryResult(
|
||||
{ gpuType: 'h100' },
|
||||
[offer({ gpuType: 'H100_80GB' }), offer({ gpuType: 'H200' })],
|
||||
{ truncated: false, providerNames },
|
||||
{ truncated: false, providerNames, totalListings: LISTINGS_ON_MARKET, totalTruncated: false },
|
||||
) as InventoryReading;
|
||||
|
||||
assert.equal(reading.count, 1);
|
||||
@@ -560,20 +606,28 @@ test('the offer list is capped and the count is not', () => {
|
||||
const reading = assembleInventoryResult({}, many, {
|
||||
truncated: true,
|
||||
providerNames,
|
||||
totalListings: 20,
|
||||
totalTruncated: true,
|
||||
}) as InventoryReading;
|
||||
|
||||
assert.equal(reading.count, 20);
|
||||
assert.equal(reading.listings.length, 8);
|
||||
assert.equal(reading.listings[0]?.onDemandPricePerGpuHourCents, 281);
|
||||
assert.match(reading.headline, /At least 20 purchasable listing\(s\)/);
|
||||
assert.match(reading.headline, /20 of at least 20 purchasable listing\(s\) on the market/);
|
||||
assert.equal(reading.scope.truncated, true);
|
||||
});
|
||||
|
||||
test('no matching offer is reported as an absence, not as an empty market', () => {
|
||||
const reading = assembleInventoryResult({ gpuType: 'MI300X' }, [offer({ gpuType: 'H200' })], {
|
||||
truncated: false,
|
||||
providerNames,
|
||||
totalListings: LISTINGS_ON_MARKET,
|
||||
totalTruncated: false,
|
||||
}) as InventoryReading;
|
||||
|
||||
assert.equal(reading.count, 0);
|
||||
assert.match(reading.headline, /No provider is currently listing capacity matching that request for MI300X/);
|
||||
assert.match(
|
||||
reading.headline,
|
||||
new RegExp(`None of the ${LISTINGS_ON_MARKET} purchasable listing\\(s\\) on the market matches`),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user