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.

PR dealops#6224 Author @yijunz166 Files 19 +/− +2306 / −213 Area dealops3/configAgent Status Open

What it adds

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.

What it changes

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.

What it preserves

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

Orchestrator
Product Catalog Agent
Approval Rule Agent
Pricebook Agent (new)
User Management Agent

1. Why this exists

Problem

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.

Fix

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.

Bonus

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.

0 pricebooks

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.

1 pricebook

Proceed, but name the pricebook in the action plan content so approving the plan doubles as pricebook confirmation.

2+ pricebooks

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.

Deterministic backstop. Even if the orchestrator forgets to ask, 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 kindBlocks 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

READ

listPricebooksWithUsageForAgent — every active pricebook enriched with live/staged product counts, up to 10 sample skuIds (with skuIdsTruncated flag), and observed currencies.

READ

getPricebookForAgent — one pricebook by id or case-insensitive name. Fails safe on ambiguous names (returns the candidate ids rather than picking arbitrarily).

WRITE (LIVE)

createPricebookForAgent — duplicate-name guard, CRM link duplicate warning (not block — multi-currency setups legitimately share one Salesforce Pricebook2).

WRITE (LIVE)

updatePricebookForAgent — name, description, CRM link. Renames keep the pricebook id stable so product references are unaffected. null clears; undefined keeps.

WRITE (LIVE)

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.

SHARED (orchestrator)

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.

Before

withSpecLock(orgSlug, spec, fn) — acquires both the per-resource mutex and the org manifest mutex. Correct for spec drafts; overkill for relational writes.

After (new)

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.

Documented race. Both locks are per-worker-process. Production runs many cluster workers, so two concurrent same-name creates on different workers can still both pass the name-uniqueness check. The comment in 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

10. Risks & open questions

Cross-worker name race (documented, accepted). The app-side name uniqueness check is check-then-act, and both write locks are per-worker-process. Two concurrent approved same-name creates on different cluster workers can both pass. The comment in pricebookContext.ts notes this needs a partial unique index migration to fully resolve. getPricebookForAgent's ambiguity handling keeps the system safe in the meantime.
Prompt weight. The orchestrator system prompt gained a substantial PRICEBOOK SELECTION section plus a PRICEBOOK MANAGEMENT section, and the planner prompt gained pricebook-resolution items in three playbooks. Prompt bloat is real; watch sub-agent latency and total token count on the next few production traces.
Test coverage is thorough. Four new spec files totaling ~1150 lines: unit tests for context, end-to-end lifecycle covering publish semantics, shared-Prisma test utils with a Prisma-faithful where-matcher, and a Gemini schema sanitizer check. The lifecycle test explicitly walks the "staged move blocks delete until published" case that the reference semantics comment describes.