Pricebook Agent joins the CPQ Config Agent
A new specialist sub-agent for pricebook CRUD, plus orchestrator wiring that resolves the target pricebook before any product-create action plan.
A fourth specialist agent — the Pricebook Agent — with five LangChain tools for live CRUD on the Pricebook table, plus a shared listPricebooks tool the orchestrator uses proactively.
Orchestrator now delegates to four agents (was three), asks users to pick a pricebook before creating products, and offers to create one when none exist. Sub-agent boilerplate collapses into one shared runner.
Product/attribute/approval-rule drafts keep their draft/publish workflow. Pricebook writes are the exception: they land live immediately — same model as approval groups.
Entity legend
1. Why this exists
Pricebooks were a blind spot. The orchestrator could stage products but had no way to inspect, create, rename, CRM-link, or delete the pricebooks those products belong to. On an org with zero pricebooks, product creation silently failed; on an org with several, it picked arbitrarily.
Introduce a Pricebook sub-agent with CRUD tools, and teach the orchestrator to resolve the pricebook first: confirm the only one, ask when several exist, or offer to create one when none do.
Four near-identical sub-agent invocation blocks (product catalog, approval rule, user management, and now pricebook) collapse into one runSubAgent helper. Fixes to extraction, telemetry, or limits now happen once.
2. The four-agent delegation model
Before this PR the orchestrator delegated to three specialists. Pricebook management had no owner — the orchestrator either lacked the capability or had to shoehorn it into the Product Catalog Agent's turn. Now it's its own surface with its own approval type (specType: "pricebooks").
3. The pricebook resolution flow
The most consequential orchestrator change is a new rule: before proposing any action_plan that creates products, resolve the target pricebook. The branch is deterministic on the count returned by listPricebooks.
Don't stage products. Offer to create a pricebook first via a separate action_plan (specType: "pricebooks"). After approval, delegate to askPricebookAgent, then propose a second action plan for the products.
Proceed, but name the pricebook in the action plan content so approving the plan doubles as pricebook confirmation.
Ask via askStructuredQuestion — one option per pricebook, letter ids A/B/C…, description as hint. Map the pick back to a pricebook id, then restate the choice by name in the action plan.
stageCreateCatalogProduct throws a specific error on ambiguous pricebook count. See pricebookLifecycle.spec.ts cycle 2 — "blocks with no pricebook, auto-attaches with one, asks with several, honors the chosen id".
4. Reference semantics — the delete guard
Pricebook deletion is the interesting part. Because pricebook writes are live-instant but product writes are drafts, a pricebook counts as referenced by the union of live and staged product rows. The comment block at the top of pricebookContext.ts is the canonical spec.
| Reference kind | Blocks delete? | Released by |
|---|---|---|
Live product row via pricebookId or systemAttributes.pricebookIds |
Yes | Publishing a staged move/delete of that product. Not by staging alone. |
| Staged-only (unpublished) draft product | Yes | Re-pointing the draft to another pricebook, or discarding the draft. No publish needed. |
ProductFamily.pricebookIds |
Yes | Editing the family configuration. |
PricingQuote historical references |
No | Deliberately never blocks — quotes pin historical pricing; soft-deleted rows remain readable for them. |
__sample__ template product |
No | Filtered out of usage aggregation entirely. |
A product mid-move counts under both pricebooks until the move publishes. That's why liveProductCount and stagedOnlyProductCount are reported separately — the sub-agent's reason string tells the user exactly which lever to pull for each kind.
5. The five pricebook tools
listPricebooksWithUsageForAgent — every active pricebook enriched with live/staged product counts, up to 10 sample skuIds (with skuIdsTruncated flag), and observed currencies.
getPricebookForAgent — one pricebook by id or case-insensitive name. Fails safe on ambiguous names (returns the candidate ids rather than picking arbitrarily).
createPricebookForAgent — duplicate-name guard, CRM link duplicate warning (not block — multi-currency setups legitimately share one Salesforce Pricebook2).
updatePricebookForAgent — name, description, CRM link. Renames keep the pricebook id stable so product references are unaffected. null clears; undefined keeps.
deletePricebookForAgent — guarded soft-delete. Returns blocked:true with a reason string when referenced. force:true reports orphaned skus/families and warns when it was the org's last pricebook.
listPricebooks in sharedTools.ts — same read as tool #1, but exposed to the orchestrator directly so it can resolve pricebooks without delegating.
6. Serialization: withResourceLock
Pricebook writes never touch the versioning manifest — they're plain relational rows. Reusing the existing withSpecLock would serialize them against unrelated draft saves for no reason.
withSpecLock(orgSlug, spec, fn) — acquires both the per-resource mutex and the org manifest mutex. Correct for spec drafts; overkill for relational writes.
withResourceLock(orgSlug, 'pricebooks', fn) — resource mutex only. Pricebook writes on org X can't interleave each other, but they don't block spec draft saves on org X.
pricebookContext.ts flags this as accepted risk pending a partial unique index migration; getPricebookForAgent fails safe on the resulting ambiguity rather than picking arbitrarily.
7. Sub-agent runner consolidation
Three existing sub-agent files (productCatalogAgent.ts, approvalRuleAgent.ts, userManagementAgent.ts) all contained an identical ~50-line block: build the ReAct agent, invoke with limits, walk messages backwards to find the final AI text, log telemetry. The new file extracts it.
// subAgentRunner.ts — the whole file, in essence
export async function runSubAgent(params: {
agent: string; // telemetry label
tools: any[];
systemPrompt: string;
question: string;
config: AgentRuntimeConfig;
signal?: AbortSignal;
}): Promise<string> {
const agent = createReactAgent({ llm: createSubAgentLLM(), tools, prompt: systemPrompt });
const result = await agent.invoke(
{ messages: [new HumanMessage(question)] },
{ recursionLimit, signal, timeout },
);
const toolsUsed = extractToolCallNames(result.messages);
const raw = extractFinalAiText(result.messages) || '{"error":"..."}';
const augmented = augmentWithToolsUsed(raw, toolsUsed);
logSubAgent({ agent, orgSlug, iterations, ... });
return augmented;
}
The per-agent files now contain only their system prompt and tool set. approvalRuleAgent.ts drops from 70 lines of scaffolding to 17. extractFinalAiText moves to subAgentCommon.ts — the Gemini array-content flatten quirk documented once, applied everywhere.
8. Type-level single source of truth
The specType vocabulary for action_plan was previously repeated in three places: the TypeScript union, the main orchestrator prompt, and the active-plan prompt fragment. Adding "pricebooks" without a central constant would drift.
// types.ts
export const PLANNED_ACTION_SPEC_TYPES = [
'catalogProducts', 'pricingSpec', 'workflowSpec',
'datasheet', 'attributes', 'pricebooks', // ← added
] as const;
export type PlannedActionSpecType = (typeof PLANNED_ACTION_SPEC_TYPES)[number];
Prompt strings now interpolate ${PLANNED_ACTION_SPEC_TYPES.join('|')}. Editing the array updates the TS union and both prompts.
9. What it doesn't change
- Draft/publish workflow for products, attributes, pricing, approval rules. Still drafts. The user still publishes via the V3 Admin UI.
- Approval group live-write model. Unchanged — pricebooks now share this model, not replace it.
- Pricebook
selectorTagsand CRM opportunity mappings. Explicitly out of scope. The sub-agent politely declines and suggests contacting an engineer/admin. - Attaching products to pricebooks. That's a product update (draft), delegated to the Product Catalog Agent — not a pricebook write.
- The Pricebook schema. No Prisma migration in this PR. The composite PK
[id, organizationId],isDeletedsoft delete, and non-unique name index are all pre-existing. - Client UI beyond one type addition.
useConfigAgent.tsadds'pricebook'to theClientPlanOwnerAgentunion — that's it.
10. Risks & open questions
pricebookContext.ts notes this needs a partial unique index migration to fully resolve. getPricebookForAgent's ambiguity handling keeps the system safe in the meantime.