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:
@@ -9,12 +9,16 @@ import {
|
||||
demandDeals,
|
||||
type Database,
|
||||
} from '@pig/db';
|
||||
import { and, desc, eq, inArray } from 'drizzle-orm';
|
||||
import { and, count, desc, eq, inArray } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import { resultScope } from './page-tools';
|
||||
import { defineTool, type AgentTool } from './provider';
|
||||
|
||||
const noInput = z.object({}).strict();
|
||||
|
||||
/** The per-collection cap here, matching `RELATED_LIMIT` in chat-tools. */
|
||||
const RELATED_LIMIT = 100;
|
||||
|
||||
export function createAccountLifecycleTool(db: Database, accountId: string): AgentTool {
|
||||
return defineTool({
|
||||
name: 'pig_get_account_lifecycle',
|
||||
@@ -23,11 +27,17 @@ export function createAccountLifecycleTool(db: Database, accountId: string): Age
|
||||
execute: async () => {
|
||||
const [account] = await db.select().from(accounts).where(eq(accounts.id, accountId)).limit(1);
|
||||
if (!account) throw new Error('The account in focus no longer exists.');
|
||||
const [deals, paperwork, recentActivity] = await Promise.all([
|
||||
db.select().from(demandDeals).where(eq(demandDeals.accountId, accountId)).limit(100),
|
||||
db.select().from(contracts).where(and(eq(contracts.accountId, accountId), eq(contracts.side, 'demand'))).limit(100),
|
||||
const [deals, paperwork, recentActivity, dealsOnBook] = await Promise.all([
|
||||
db.select().from(demandDeals).where(eq(demandDeals.accountId, accountId)).limit(RELATED_LIMIT),
|
||||
db.select().from(contracts).where(and(eq(contracts.accountId, accountId), eq(contracts.side, 'demand'))).limit(RELATED_LIMIT),
|
||||
db.select().from(activities).where(eq(activities.accountId, accountId)).orderBy(desc(activities.occurredAt)).limit(1),
|
||||
// The denominator. This result is one account's slice of the book and
|
||||
// every count in it is an account count; without the book's own figure
|
||||
// beside them, "4 demand deals" is the only deal number in the payload
|
||||
// and becomes the answer to a question about the whole book.
|
||||
db.select({ value: count() }).from(demandDeals),
|
||||
]);
|
||||
const demandDealsOnBook = dealsOnBook[0]?.value ?? 0;
|
||||
const dealIds = deals.map((deal) => deal.id);
|
||||
const contractIds = paperwork.map((contract) => contract.id);
|
||||
const [requests, reservations, obligations] = await Promise.all([
|
||||
@@ -36,7 +46,18 @@ export function createAccountLifecycleTool(db: Database, accountId: string): Age
|
||||
contractIds.length ? db.select().from(contractObligations).where(inArray(contractObligations.contractId, contractIds)) : [],
|
||||
]);
|
||||
return {
|
||||
scope: resultScope({
|
||||
covers: `belong to the account ${account.name}`,
|
||||
matched: deals.length,
|
||||
total: demandDealsOnBook,
|
||||
totalLabel: 'demand deal(s) on the book',
|
||||
listed: 0,
|
||||
filters: { accountId, side: 'demand', rowCapPerCollection: RELATED_LIMIT },
|
||||
truncated: deals.length >= RELATED_LIMIT || paperwork.length >= RELATED_LIMIT,
|
||||
}),
|
||||
account: { id: account.id, name: account.name },
|
||||
demandDealsForThisAccount: deals.length,
|
||||
demandDealsOnBook,
|
||||
lifecycle: evaluateCustomerLifecycle({
|
||||
accountId,
|
||||
deals: deals.map((deal) => ({ ...deal })),
|
||||
@@ -47,7 +68,7 @@ export function createAccountLifecycleTool(db: Database, accountId: string): Age
|
||||
lastActivityAt: recentActivity[0]?.occurredAt ?? account.lastActivityAt,
|
||||
lastActivityId: recentActivity[0]?.id,
|
||||
}),
|
||||
interpretation: 'Scores rank review attention. Capacity totals mean sold or reserved capacity, not customer workload utilization.',
|
||||
interpretation: 'Scores rank review attention. Capacity totals mean sold or reserved capacity, not customer workload utilisation. Every figure here covers this one account, never the book.',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user