overview / agentic-workflows / 02-solution-strategy
Solution Strategy Workflow
Last updated at 2026-07-30 AEST.
A high-level walkthrough of how the platform turns a user's "Generate solution strategy" click into a stored, versioned, self-evaluated strategy that downstream features (solution generation, the solution builder UI) can build on.
At a glance
A handful of numbers capture the shape of the workflow:
Overview
The solution strategy is the upstream design decision: it captures which service categories, vendors, product families, delivery motions and service wrappers the platform recommends for this deal, structured as JSON. A user reviews, overrides, and approves a strategy version; solution generation then grounds on the approved, non-stale one. The characteristics below are grounded in the mechanisms documented in the rest of this file β they describe what the code actually does, not aspirations.
Every run scores its own output: eleven deterministic scorers mirror the generation prompt's hard rules, a set of deterministic correctors repairs the mechanically-fixable violations in place, and a frontier judge agent adds the four subjective qualities code can't check.
When a round misses the quality bar and at least one of its HIGH findings is routable, the loop regenerates the whole strategy with the findings as corrective guidance and re-scores β capped by a revise budget and stopped early on a plateau or an unfixable finding. The best round wins.
If the deal's latest context no longer matches its current approved requirement set, the run's first phase re-synthesizes that context inline β a nested run of the full deal-context workflow β so the strategy is never built on stale requirements.
The mutation is fire-and-forget: a small validated kickoff that hands off to a Pub/Sub topic. In every deployed environment the work runs in a dedicated Cloud Run Job execution, sized for the nested deal-context refresh it may perform.
Each visible step caches its result and short-circuits on WorkflowStep.completedAtUtc; on retry only the unfinished steps re-run. The refinement loop's round history lives in Mastra-snapshotted state and is simply recomputed on a redelivery.
The job is claimed atomically so two workers can't double-process, and SolutionStrategy rows are a new version per run β nothing overwritten β so a partial retry can never corrupt an earlier version. Version races resolve by a unique-constraint retry, not an advisory lock.
Failures are classified deterministic versus transient; the pull and Cloud Run Job paths route them oppositely (see Failure handling). Cancellation exits cleanly, including mid-refresh.
Unlike proposal generation, the mutation creates no placeholder output row, takes no advisory lock, and does not advance the deal stage β those are deferred to the runner's success path. The frontend tracks live progress from the AsyncProcessingJob row alone.
Every run pins an immutable SolutionContextSnapshot of its exact inputs, writes a Generation audit row for the main agent call, and links each version to the snapshot, the deal context (and its captured version), the generation, an approved-requirements fingerprint, and its own evaluation.
Path overrides, commercial selections, and prose edits layer over the frozen AI value, which is never mutated. Editing auto-unapproves; acknowledging a finding does not.
All data is scoped to the caller's organization, and the terminal state transition is published over Redis pub/sub (solutionStrategyChanged) so the new version appears live across every open client tab without a manual refresh.
Identity is taken from the auth context, never from the caller. The agent may only reference categories, vendors, product families, and service classes present in the compiled context β and where the prompt alone isn't enough, the correctors and scorers enforce it deterministically after the fact.
The remainder of this document walks the flow end to end, from the triggering mutation through to where the finished strategy is stored.
1. Entry Point
The user triggers strategy generation from the web app, which calls a single GraphQL mutation on the API:
- Mutation:
initiateSolutionStrategyGeneration
This is a "fire-and-forget" mutation: it returns almost immediately with a job handle. The actual generation runs asynchronously in the background, and the frontend tracks progress through GraphQL subscriptions.
2. Input Required
The mutation accepts the smallest possible InitiateSolutionStrategyGenerationInput:
| Field | Required | Purpose |
|---|---|---|
dealId | Yes | The deal the strategy is being generated for. Everything else (organization, user, requirements, context) is resolved from this. |
No knobs by design. There are no custom instructions or force-regeneration flags at this layer β every run produces a fresh strategy version, and the refinement loop's behaviour is environment configuration, not a per-request option.
The logged-in user is taken from the auth context β the resolver never trusts a caller-supplied user or organization ID.
Preconditions
Before the resolver enqueues anything, it validates that the deal is in a state that can actually produce a strategy:
- The deal exists and belongs to the caller's organization.
- The deal has at least one approved deal requirement. The strategy is built against the approved set only β drafts and rejected requirements are excluded.
- The deal has at least one deal context.
If any check fails, the mutation throws a BadRequestException with a human-readable reason β nothing is queued. The approved-requirements check is re-run inside the runner, because it could have raced against a requirement being un-approved between the mutation and message dispatch.
The "kickoff" work
Compared to proposal generation, the kickoff is deliberately lightweight:
- Creates an
AsyncProcessingJobof typeGenerateSolutionStrategy, linked to the deal. - Publishes the Pub/Sub message to the strategy generation topic.
There is no placeholder output row, no advisory lock, and no immediate deal-stage advance at kickoff β those are intentionally deferred to the success path inside the runner. The frontend tracks live progress via the AsyncProcessingJob row alone.
3. The Pub/Sub Message
The resolver publishes a message to a Google Cloud Pub/Sub topic. This is the hand-off from the synchronous request to the background execution path.
- Topic:
<environment>.deal-output.generate-solution-strategy(the literal name is environment-prefixed and resolved per environment via env vars) - Schema:
solutionStrategyGenerationPubSubMessageDataSchema
Payload:
{
"correlationId": "<asyncProcessingJobId>",
"payload": {
"asyncProcessingJobId": "...",
"dealId": "...",
"userId": "..." // who triggered the run
}
}
The message is tiny β just three pointers. Everything else is resolved from the database. No business data is carried on the wire.
4. How the Message Is Executed
As with every long-running workflow on the platform, there are two execution paths, selected per workflow by the WORKFLOW_PUSH_JOBS_ENABLED env var. Solution strategy is in that set in dev and both production regions:
- Push path (deployed). A push subscription POSTs the message to the OIDC-gated
workflow-dispatcherCloud Run service, which claims the job with a compare-and-swap (PendingβInProgress) and then starts a dedicated Cloud Run Job execution, stamping the workflow type and the verbatim base64 message data as container-override env vars. The Job's entrypoint boots a slim Nest application context (runner services only β no HTTP, no GraphQL, no subscribers), re-validates the payload through the same Zod schema, publishes theInProgressevent (the dispatcher has no Redis), and drives the shared runner. - Pull path (local). An in-process streaming-pull listener inside the API claims the job and drives the same shared runner.
Why this Job is sized like the deal-context Job (4Gi container, a --max-old-space-size=2560 V8 heap): it runs the deal-context generation workflow in-process as its leading refresh phase, so it inherits that workflow's retained-set profile. Skipping asset analysis bounds the refresh's wall-clock, not its heap β the synthesis agent's stream buffers and retained result are the same either way. Both Job descriptors are sized together for exactly this reason.
What the shared runner does
The workflow body lives in SolutionStrategyGenerationRunnerService:
- Re-read the job pointers β the deal id and organization id from the
DealAsyncProcessingJobrow. - Re-validate that approved deal requirements still exist.
- Re-check the grounding. Every delivery β a manual retry as much as an automatic redelivery β re-asserts that this run can still produce a correct strategy. It is refused in two cases: it carries completed work but the deal's latest context is no longer the one it was compiled against, or its context now reads stale against the approved requirement set and its persisted step set holds no unfinished "Refreshing requirements" step to fix that. The second case is the reason the drift recompute above cannot live only on the first delivery: a run whose step set was fixed before the requirements moved has no way to self-heal, so resuming it would generate against a context nobody approved. Both refusals are deterministic, so the message is acknowledged and the job goes terminal carrying the reason, rather than redelivering against a condition that cannot clear by itself. A run carrying nothing is never refused β it recompiles its context from scratch anyway.
- Decide the step set. On the first delivery, compute whether the deal's latest context has drifted from its current approved requirement set (see below). If it has, prepend a "Refreshing requirements" step to the workflow's own steps. A redelivery reuses the persisted
WorkflowProgressverbatim, so the step set is decided once and never rebuilt. - Initialize or restore
WorkflowProgressplus oneWorkflowSteprow per step. On retry, only the not-yet-completed steps are reset. - Run the inline deal-context refresh if the refresh step exists and hasn't already completed (section 5).
- Compile the solution context into a
SolutionContextSnapshotβ an immutable, point-in-time aggregation of everything the agent will read β then record the deal context that compile resolved ontoWorkflowProgress, which is what a later delivery compares its carried-over work against (section 6). - Start the Mastra workflow run (section 7), surfacing the run handle synchronously so a cancel arriving before the first step can still abort it.
- On completion: publish
solutionStrategyChanged, append aSolutionStrategyGenerateddeal-history activity (idempotent on the persisted strategy id), mark the jobCompleted, and advance the deal stage toSolutioningif it isn't already past it.
Failure handling
Failures are classified deterministic (a schema/contract violation surfaced as DeterministicWorkflowError, which reproduces on replay) versus transient (network, provider blip, DB disconnect, OOM). The two execution paths route them oppositely, because "retry" means something different in each:
- Deterministic failures mark the job
Failedand stop β acked on pull, exit 0 on push. - Transient failures retry. On pull the job reverts to
Pendingand the message nacks (Pub/Sub redelivers up to the retry budget, then dead-letters). On push the row staysInProgressand the process exits non-zero so Cloud Run task-retry (3 retries, 1-hour per-task timeout) resumes from theWorkflowStepcheckpoints. - A failed inline refresh throws, inheriting whichever classification the nested deal-context run produced β so the strategy is never generated against a context the refresh failed to update.
- Cancellation is checked at multiple points, including mid-refresh; a cancelled job exits cleanly without flipping to failed.
- A recovery sweep runs on startup and every 15 minutes, failing jobs stuck past their stale thresholds. On the push path the stale-
InProgressthreshold widens to 270 minutes so it can't reap a Job mid task-retry.
5. Keeping the Requirements Fresh (the inline refresh)
A strategy built on a deal context that predates the current approved requirement set is wrong before it starts. Because the solution context compiler snapshots only the latest DealContext, the fix has to happen before compilation β so the runner performs a staleness-gated inline refresh as the job's leading phase.
The staleness test reuses the exact primitives the DealContext.isStale resolver uses, so the rule is never forked: a deal with no context, or a context with no recorded requirement baseline, cannot be proven current and therefore counts as stale; otherwise the recorded snapshot is diffed against the freshly-built fingerprint of the current approved set.
When it fires, the runner starts dealContextGenerationWorkflow as a nested run inside this same job, with the requirement-change shape (no assetId) so the asset-analysis step early-returns and only the synthesis agent runs. Three properties make this safe rather than surprising:
- It surfaces as one step, not a second job. The nested run's request context points at this job's
WorkflowProgressand at the refreshWorkflowSteprow, so the live processing badge shows "Refreshing requirements" and the deal-context step's ownisStepAlreadyCompletedguard short-circuits it on replay. The step message is deliberately requirement-framed β the refresh is an internal detail of the single "Generate solution strategy" action. - It is cancel-abortable. The nested run handle is surfaced through the same registry hook; the main strategy run's handle later overwrites it (both keyed on the stable job id), so a cancel always aborts whichever run is currently active.
- It publishes normally. The nested run writes a real new
DealContextversion and publishesdealContextChanged, so every open tab sees it.
This path deliberately does not take the per-deal deal-context job lock β it runs under the already-claimed strategy job. A concurrent asset-analysis GenerateDealContext job could therefore create a version alongside it; that is tolerable because DealContext is additive-versioned and the latest version wins at compile time.
6. Data Retrieved for the Run
Before the workflow runs, the runner assembles everything the AI agent will need. All of this is read once, up front, so the workflow itself issues no ad-hoc aggregation reads mid-run:
| Source | What's read |
|---|---|
Deal | Deal ID and organization ID. |
DealRequirement (approved only) | Confirms approved requirements exist; the snapshot below pulls in the full set. |
SolutionContextSnapshot | Built fresh on every run β see below. |
DealContext (pinned via the snapshot) | The latest captured context for the deal β refreshed first if it had drifted (section 5). |
InstitutionalOverlayVersion (published) | The organization's institutional profile (active categories, vendor candidates with preference/exclusion status, product families, service offerings). |
CommercialProfileVersion (published) | The organization's commercial profile (rate cards, pricing). Used to figure out which service classes are commercially viable. |
OntologyRelease (active) | The shared canonical ontology that maps requirements to vendor/product families. |
The solution context snapshot is the heaviest piece. It's compiled by ContextCompilerService.buildSolutionContext and pulls together:
- The deal context (commercial constraints, current state, customer profile, NFRs, objectives, sizing, technology profile).
- The grounded approved requirements (mapped against the active ontology).
- The active institutional overlay β which category, vendor, product-family and service-offering candidates are valid for this organization, and which are preferred or excluded.
- The commercial availability picture: which of the five service classes (Advisory, Implementation, ManagedService, Migration, Support) are actually sellable based on the rate card.
- Completeness indicators that downstream quality gates and UI badges can read off.
The snapshot is persisted to Postgres so the exact inputs that produced a given strategy version can be replayed later if needed, and it doubles as the deterministic lookup table the correctors and scorers score against (section 8). Version races on the snapshot itself resolve by a unique-constraint retry.
7. How Data Flows Through the Workflow
The workflow is a Mastra workflow (solutionStrategyWorkflow) of three visible steps wrapped around a bounded self-refining loop:
generate β evaluate β dountil(revise-body, stop?) β restore-best-round β store
Only generate-solution-strategy, evaluate-solution-strategy and store-solution-strategy are registered as progress steps. The loop body's steps and the restore step use ids deliberately absent from that map, so they are invisible to the fixed step-count accounting and write no WorkflowStep or Generation rows β the loop's entire record lives in Mastra-snapshotted workflow state and is simply recomputed on a redelivery. To the UI, the whole loop is one opaque "Evaluating solution strategy" step: the evaluate step writes its started transition and the post-loop restore step writes its completed.
Step graph
What each step does
-
Generate solution strategy β calls the
solutionStrategyGenerationAgentwith:- The system prompt baked into the agent (rules for how to pick categories, vendors, families, delivery motions, committed paths, alternatives, and the engagement mix), whose JSON output shape is derived from the Zod output schema so the schema stays the single source of truth.
- A system message containing the full solution context snapshot payload (section 6).
- A short user prompt: "Generate a partner-specific solution strategy based on the provided solution context."
The main pass is a single
agent.generatecall withstructuredOutput, validated againstsolutionStrategyGenerationAgentOutputSchema. The full response (model used, prompts, raw text, token usage, response time, success/failure) is recorded in aGenerationrow for traceability and cost accounting. If schema validation fails, the step throws a deterministic error. -
Evaluate solution strategy β opens the opaque step and builds round 0: run the deterministic correctors, score the corrected value with the deterministic scorers, and (when refinement is enabled) run the judge agent and merge its subjective findings in. When refinement is disabled this is a purely advisory deterministic evaluation with no judge call, and the loop below is a no-op. An unparseable solution context skips evaluation entirely (a zero-finding advisory result) rather than failing the run.
-
The revise loop body (a committed sub-workflow, so it can serve as a Mastra
dountilbody) β begin decides whether this pass should actually revise and flips the revise-pass flag; regenerate re-invokes the generation agent with the round's routable HIGH findings as corrective guidance; re-evaluate scores the new value and appends the next round. The body always runs at least once (do-until semantics), so a pass that shouldn't revise simply no-ops.The revise regenerate does not reuse the main pass's
agent.generate({ structuredOutput })call. Provider enforcement of a nested output schema is unreliable at runtime, so the revise path uses the streaming route withjsonrepairand a bounded retry β the same shape the judge uses. -
Restore best round β writes the winning round's value and evaluation into the flat state fields the store step reads, and closes the opaque step. A no-op when the loop never ran.
-
Store solution strategy β looks up the deal's current max
SolutionStrategy.version, builds a fresh approved-requirements fingerprint, resolves the captured deal-context version, and persists a new row (fields in section 9). A version collision (P2002) is resolved by re-reading the max and retrying the create.
Caching, retries, and progress
- Every visible step writes a
WorkflowSteprow withstarted,completed, orfailedtimestamps. This is what powers the live progress messages the frontend shows: "Refreshing requirements" (when it applies), "Generating solution strategy", "Evaluating solution strategy", "Storing solution strategy". - On retry, a step that already completed against the same job is short-circuited β the previously persisted strategy ID is read straight from
WorkflowStep.result. - The refinement loop is deliberately not checkpointed. Its rounds live only in Mastra-snapshotted state, so a redelivery re-runs generate β round 0 β loop from scratch. That keeps the subscriber's fixed step-count accounting intact at the cost of re-spending the loop's agent calls on a redelivery.
8. The Evaluation Rubric & the Refinement Loop
The evaluation is deliberately mostly deterministic. The generation prompt states hard rules; the evaluator re-checks those same rules in code, so a violation is caught even when the model ignored the instruction. Only the residual qualities code genuinely cannot check are delegated to a judge agent.
Three layers
Deterministic repairs applied to the agent's value before it is scored or persisted β the mechanically-fixable subset, each recorded as a visible auto-correction note:
- Drop a committed path whose vendor is invented (absent from every candidate set) β there is nothing valid to fall back to.
- Re-point a committed vendor that is excluded for its category to the strongest selectable candidate; drop the path when none exists.
- Drop product families that are inactive or excluded for the (possibly re-pointed) vendor.
- Filter each path's nested alternatives, removing invented and excluded vendors.
- Filter service-wrapper ids down to those present in the commercial availability picture.
Violations needing judgement (e.g. tower coverage) are deliberately not silently rewritten β they surface as findings instead.
Eleven rules run in a fixed order over the corrected value, so the findings list is reproducible. Each mirrors a rule in the generation prompt's critical-rules block:
| Rule id | What it checks |
|---|---|
active-category-only | Every committed path attaches to a category in the institutional active set. |
no-excluded-vendor | No committed or alternative vendor is excluded for its category. |
valid-families | Product families are active and belong to their vendor. |
motion-honours-overlay | The delivery motion is one the overlay actually confirms. |
no-invented-services | Service wrappers exist in the commercial availability picture. |
no-invented-entities | Every referenced id resolves to real candidate data. |
vendor-category-candidate | Each vendor is a candidate for the category it is attached to. |
prefers-preferred | A preferred candidate wasn't passed over without cause. |
tower-coverage | Requirement towers are covered by the committed paths. |
uncoverable-routed | Requirements the profile can't cover are explicitly routed, not silently dropped. |
ids-traceable | Ids are bare cuids drawn from the context, not invented labels. |
The solutionStrategyEvaluationAgent (frontier tier) scores the corrected strategy against the compiled context and may raise findings on exactly four subjective rule ids β the residue the scorers cannot mechanically check:
rationale-soundnessβ does the stated reasoning actually support the choice?current-state-framingβ is the current state characterised honestly?alternative-distinctnessβ are the alternatives genuinely different options, not near-duplicates?confidence-calibrationβ is the stated confidence justified by the evidence?
Each judge run is bounded by a 10-minute ceiling and gets at most one corrective re-ask on a JSON-parse or schema-validation failure (a stall-guard exhaustion or empty response is not retried β it has already burned the tiers and the time budget). Configuring more than one run per round merges them deterministically, with severity-max winning on a key collision. The judge's own advisory score is not folded into the roll-up β only its findings are.
How the score is computed
The overall score is deterministic and deduction-based, not model-assigned: start at 100 and subtract per finding, deterministic and judge findings alike.
Each evaluation is stamped with the rubric version that produced it β the deterministic version alone when no judge ran, or <deterministic>+<judge> when it did β so historical scores stay attributable as the rubric evolves.
The loop's stop conditions
Round 0 is the pre-loop evaluation; each subsequent round is one completed revise pass. The loop continues only when all of these hold:
- Refinement is enabled.
- The latest round misses the quality bar β the bar being zero HIGH findings and a score at or above the threshold (default 80).
- At least one of its remaining HIGH findings is routable.
- The revise budget (
maxIterations, default 2) is not yet spent. - The round has not plateaued β it improved either the HIGH count or the score versus the round before it.
Routability is the interesting one. A revise pass works by re-invoking the generation agent, so a finding is routable only if regeneration could plausibly fix it: the deterministic contract violations (the agent can re-select a valid path) plus rationale-soundness, current-state-framing and alternative-distinctness (it can re-write the prose or re-pick a genuinely distinct alternative). confidence-calibration is deliberately excluded β a regeneration that restates the same thin grounding cannot honestly raise the evidence its calibration is judged against. So a round whose only HIGH finding is a calibration miss stops the loop instead of burning a pass, and the unrouteable findings are logged for diagnosis rather than silently inflating cost.
Best round = fewest HIGH findings, then highest overall score, with ties resolving to the earliest round β a later round must strictly beat the incumbent to win, which spares already-spent agent calls.
Configuration
| Setting | Default | Effect |
|---|---|---|
β¦REFINEMENT_IS_ENABLED | true | When off, the evaluate step produces a deterministic-only advisory evaluation and the loop is a no-op. |
β¦REFINEMENT_MAX_ITERATIONS | 2 | Revise-pass budget. |
β¦REFINEMENT_SCORE_THRESHOLD | 80 | The score half of the quality bar. |
β¦REFINEMENT_IN_LOOP_JUDGE_RUN_COUNT | 1 | Judge runs merged per round. |
β¦REFINEMENT_SEVERITY_GATE | high | Plumbed end to end but not yet consumed β the loop gates exclusively on HIGH findings today. |
The runner stages these onto the Mastra request context before the run starts, because Mastra steps can't read Nest config directly.
9. Where the Generated Strategy Is Stored
Unlike proposal generation, there is no file output and nothing is uploaded to Google Cloud Storage. The solution strategy is purely structured data: a row in the SolutionStrategy table whose value column holds the strategy JSON.
| Column | What it carries |
|---|---|
value | The frozen AI strategy JSON (the best round's value). Never mutated by consultant edits. |
evaluation | The winning round's evaluation: findings, auto-corrections, overall score, rubric version, and the consultant's acknowledged finding keys. |
generationId | The main agent call that produced it (1:1). |
solutionContextSnapshotId | The exact compiled inputs β replayable. |
dealContextId | The deal-context version pinned by the snapshot. |
capturedDealContextVersion | That version's number, so staleness can later detect deal-context drift (a strictly newer version exists). |
approvedRequirementsSnapshot | A fingerprint of the approved requirement set at generation time β the requirement-drift half of staleness. |
userSelections | Consultant overrides: excluded service offerings, delivery-motion toggles, selected path per category, prose edits. Layered over value on read. |
isApproved / approvedAtUtc / approvedByUserId | The approval state and who set it. |
version | The new sequential version, unique per deal. |
Once the workflow finishes successfully, the runner:
- Loads the newly-created
SolutionStrategyby ID. - Publishes a
solutionStrategyChangedCREATED event through Redis pub/sub so every view subscribed to it (the deal page, the solution builder card, sibling tabs) evicts its cache and refetches before the next step. - Appends a
SolutionStrategyGenerateddeal-history activity (idempotent, keyed on the persisted strategy ID, so a redelivery can't duplicate it) so the deal-history view has it on the wire before the job flips toCompleted. - Marks the
AsyncProcessingJobasCompletedso the frontend's "Generatingβ¦" UI state clears. - Advances the deal stage to
Solutioningif the deal is not already past it.
What the consultant can do with it
The frontend reads the SolutionStrategy row to render the strategy itself (categories, committed paths, alternatives, delivery motion, service wrappers, engagement mix) plus its evaluation panel, and exposes a set of mutations that all layer over the frozen value:
- Override the committed vendor path for one service category (selecting the committed vendor again clears the override).
- Update the commercial selection β delivery-motion toggles, excluded service offerings. Passing
nullclears an override and falls back to the recommended value. - Layer prose edits over the AI-generated narrative.
- Acknowledge (or un-acknowledge) an individual finding by its deterministic key.
- Approve / unapprove the version.
The semantics are deliberately asymmetric: editing the selection or the prose auto-unapproves the strategy, while acknowledging a finding keeps approval (it is a judgement about a known issue, not a change to the recommendation). Acknowledgements are stored inside the evaluation, so they auto-clear when the strategy is regenerated. Unapproving is blocked when the strategy is already used by a generated proposal or a proposal generation is in flight.
10. Where This Sits in the Chain
Approval of a strategy is not the proposal gate β it is the solution-generation gate:
- Solution generation (see Solution Generation) grounds on the deal's usable approved strategy: approved and non-stale. A stale approved strategy is not valid grounding, so it counts as no strategy at all, and the solution run is refused rather than generating on bad inputs β on the initial request and on every later delivery alike.
- Proposal generation (see Proposal Generation) gates on an approved, non-stale
SolutionModelversion β and derives the strategy from that model's link, rather than picking the approved strategy itself.
Staleness is evaluated on two axes across the whole chain, always with the same shared primitives: requirement drift (the recorded approvedRequirementsSnapshot versus the current approved fingerprint) and deal-context drift (a strictly newer DealContext.version than the captured one; a null captured version is deliberately not stale).
11. Security
The flow handles customer-supplied content and the organization's own institutional and commercial profile, so security applies at the API boundary, on the wire, at the dispatch hop, in storage, and inside the agent.
- Identity is never caller-supplied. The triggering mutation derives the logged-in user β and therefore the organization β from the authenticated request context; it never trusts a user or organization ID sent by the caller. Every database read and write in the kickoff and the runner is scoped to that resolved
organizationId. - Input is handled at the boundary.
InitiateSolutionStrategyGenerationInputcarries only adealId, trimmed at the resolver viaTrimPipe, and the resolver enforces the section-2 preconditions before anything is queued. (ThedealIdfield itself has noclass-validatorlength/format check; the preconditions' org-scopedfindFirstOrThrowis what rejects a non-existent or cross-org id.) - The dispatch hop is authenticated and minimally privileged. Every dispatcher route is gated by
GoogleOidcGuardagainst a fixed audience, and the dispatcher holds only the database-connection secret β never the API's full secret set. - The Pub/Sub message carries no business data. Only the three pointer IDs cross the wire (section 3); the runner re-reads everything from the database under the job's organization scope.
- The strategy never leaves the database. There is no file output and no bucket β the strategy is a
SolutionStrategyrow in the org-scoped Postgres schema, with no signed URLs or external artifacts to secure. - Real-time events stay within the organization. The
solutionStrategyChangedevent is fanned out only to clients in the publishing organization, following the platform's subscription-payload conventions. - The agent is constrained to grounded inputs β and the constraint is enforced twice. Its system prompt forbids recommending categories, vendors, product families, or service classes absent from the compiled context, forbids excluded vendors and unconfirmed delivery motions, and forbids inventing identifiers (ID fields must carry bare cuids drawn from the context). Because a prompt-level control can be ignored, the deterministic correctors and scorers re-check the same rules against the snapshot's lookups after generation β repairing what is repairable and surfacing the rest as findings. Untrusted deal content is supplied as context data the agent reasons over, never as instructions.
- Query-cost budgets apply. The mutation and its companion subscriptions run under the API's global GraphQL depth/complexity/token limits.
12. Performance & Cost Controls
Solution strategy generation is lighter than proposal generation, but the refinement loop means it is no longer a single-call workflow β so it is bounded on several axes.
- Full process isolation. The deployed push path gives each run its own Cloud Run Job execution (4Gi container, 2560MB V8 heap, 1-hour task timeout), sized for the nested deal-context refresh it may perform, so no HTTP request is held open and a slow run never ties up request capacity.
- The refresh is conditional and bounded. It runs only when requirements have actually drifted, and its requirement-change shape skips asset analysis entirely β one synthesis agent call rather than a full re-ingestion.
- Inputs are read once, up front. The runner compiles the entire solution context before the workflow starts, so the steps issue no ad-hoc aggregation reads mid-run. The compile is version-collision-safe and reused as the scorers' lookup source.
- Refinement is cost-bounded and stops early. The revise budget caps the passes; the quality bar, unrouteable-findings and plateau conditions usually stop it sooner; best-round ties resolve to the earliest round; and the judge is skipped entirely when refinement is disabled or the context won't parse.
- The evaluation is mostly free. Eleven of the fifteen rules are pure code over an already-loaded snapshot β no tokens. Only the four subjective ones cost a model call.
- Bounded agent calls. Every judge run has a 10-minute ceiling with at most one corrective re-ask; stall-guard exhaustion is not retried. The generation and revise calls run under the shared stall guard with its 13-minute wall-clock ceiling and idle-liveness window.
- Step-level short-circuit and selective retry. Each visible step records its result and short-circuits on
completedAtUtc; a retry re-runs only the steps that hadn't completed. The loop is the deliberate exception β it is recomputed rather than checkpointed. - No rendering or upload tail. Because the output is a database row and not a rendered document, there is no DOCX rendering, GCS upload, or signed-URL minting cost on the success path.
Summary
End-to-end the flow looks like: