feat(coderd/x/chatd/chatadvisor): add advisor runtime and tool wrapper#24620
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
b553eb7 to
c421906
Compare
bd3e0bc to
2ae36ac
Compare
c421906 to
6d3c573
Compare
2ae36ac to
43ac7dd
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 43ac7dd9d3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
6d3c573 to
ddfd162
Compare
43ac7dd to
c9441c8
Compare
ddfd162 to
51dbd80
Compare
c9441c8 to
d387ec4
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2ae71a0474
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
51dbd80 to
2f6a4e5
Compare
2ae71a0 to
bdf25bb
Compare
2f6a4e5 to
41a1c64
Compare
bdf25bb to
822da65
Compare
|
@codex review |
|
/coder-agents-review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7cdbb474f3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Clean package structure, well-scoped to the stated PR 2 boundary. The separation from chattool, the use of chatloop.Run in one-step mode, and the regression tests for the provider-options cloning bug are all solid. Six new files, each with a clear single responsibility.
Severity count: 8 P2, 14 P3, 5 Nit.
The P2 findings cluster around three themes: (1) the handoff truncation strategy produces non-contiguous context with invisible gaps, (2) the advisorResultMap/jsonToolResponse chain duplicates what json.Marshal already does, justified by a false cyclic-import claim, and (3) transient failures permanently consume advisor budget slots. The orphaned-stored-responses finding (DEREM-9) is a real provider-side consequence of inheriting Store: true without clearing it.
Test coverage is strong for the happy path but thin for error variants through the tool layer and for the truncation boundary behavior. The truncation test (DEREM-10) accepts 90% message loss without catching it.
Process note: the PR description claims five defensive assertions, but two do not exist in the code ("known result variants" is structural, not asserted; "non-negative remaining uses" is arithmetic clamping, not an assertion), and one asserts the value of its own literal (the panic guards at runner.go:59). The final state is correct, but the safety inventory in the description overstates the runtime checks.
"The backward walk skips any message whose JSON size exceeds remainingChars but keeps searching older, smaller messages. This is bin-packing, not recency selection." (Hisoka)
🤖 This review was automatically generated with Coder Agents.
41a1c64 to
e17e45b
Compare
7cdbb47 to
a9271fa
Compare
1d49276 to
4c251ef
Compare
|
@codex review |
|
/coder-agents-review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c059b0b903
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return AdvisorResult{}, xerrors.New("advisor question is required") | ||
| } | ||
|
|
||
| if !rt.tryAcquire() { |
There was a problem hiding this comment.
Charge advisor quota only after successful nested run
RunAdvisor consumes one use via tryAcquire() before the nested model call executes. When chatloop.Run fails or returns no text, the function emits an error result but still burns quota, so a transient provider failure can immediately force later attempts into limit_reached (for example, MaxUsesPerRun=1 means no retry is possible). This also conflicts with the runtime contract that usage increments on successful calls.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Rebase only (zero diff in chatadvisor/ between R10 and R11). Netero and all 4 panel reviewers confirm: no open findings. Tests pass clean.
This is the fifth consecutive clean round (R7, R8 code-clean, R10, R11). Across 11 rounds, 33 findings raised, all resolved. The package is stable.
🤖 This review was automatically generated with Coder Agents.
4c251ef to
f4bf5c6
Compare
c059b0b to
6f4932b
Compare
f4bf5c6 to
ea22b45
Compare
6f4932b to
6d4e314
Compare
Merge activity
|
Adds a new chatadvisor package that wraps chatloop.Run in a single-step, tool-less nested model call for strategic guidance during a chat turn. - types.go: AdvisorArgs, AdvisorResult, and result type constants - guidance.go: nested system prompt + parent guidance block - runtime.go: RuntimeConfig + Runtime with atomic usage counter - handoff.go: BuildAdvisorMessages with truncation - runner.go: RunAdvisor performs the nested tool-less call The runtime asserts no tools are passed to the nested chatloop.Run invocation, which protects against accidental recursion into action tools. --- _Generated with `mux`_
Thin fantasy.AgentTool wrapper over chatadvisor.Runtime. Lives in the chatadvisor package (not chattool) so the advisor can depend on chatloop without a cycle: chattool transitively depends on chatprompt, which depends on chattool for attachment helpers; keeping the advisor tool inside chatadvisor sidesteps that loop. Validates the question is non-empty after trim and bounded to 2000 runes, then serializes the AdvisorResult as a JSON tool response. --- _Generated with `mux`_
… options maps.Clone only copies the ProviderOptions map shell, so pointer entries like *fantasyopenai.ResponsesProviderOptions are shared with the parent chat run. chatloop.Run mutates that struct via clearPreviousResponseID on chain-mode exit, so an advisor invocation could clear the parent loop's PreviousResponseID mid-turn. Add cloneProviderOptions that deep-copies the known in-place mutated entry (OpenAI Responses) and falls through for other types. Cover the behavior with a test that verifies the parent's PreviousResponseID survives an advisor run. Change-Id: Ibe7645cad29f21f748945b0bde87b6d5c4227f7e Signed-off-by: Thomas Kosiewski <tk@coder.com>
…nvocation The Runtime-scoped clone introduced in the prior fix is still mutated by chatloop.Run on chain-mode exit. With a long-lived Runtime, that means the first RunAdvisor call observes the parent's PreviousResponseID, but subsequent calls silently run without it, producing inconsistent behavior. Clone rt.cfg.ProviderOptions per invocation inside RunAdvisor and strip chain-mode markers (PreviousResponseID) before the nested call. The advisor passes full history from BuildAdvisorMessages, so chain mode would be incorrect for it regardless of parent state. Change-Id: I02939df5a9570eb95c0dd3908ea3d07c03a0d173 Signed-off-by: Thomas Kosiewski <tk@coder.com>
…sor runtime and tool - Stop advisor truncation walk at oversized messages so the forwarded window stays contiguous; previously a skipped oversize left a gap where later messages could reference missing context. - Also force Store=false on reset provider options so nested advisor calls do not leave orphaned stored responses on the provider. - Marshal AdvisorResult directly instead of mirroring its fields into a map; introduce a named ResultType so variants are compile-checked. - Trim RunAdvisor to a single public-API check (empty question) and drop vacuous validations/panics that NewRuntime already guarantees. - Tighten truncation, error, limit-reached, and empty-output tests; add tool-layer coverage for error and limit-reached serialization. - Drop the 'stronger model' claim from the tool description and align error wording, doc comments, and helper names with what is actually measured or enforced. Change-Id: Ia6fca65f48cad93d29f21ccc7ab460fcfd83348e Signed-off-by: Thomas Kosiewski <tk@coder.com>
…sor budget messageJSONByteCount and advisorConversationJSONByteBudget are named and documented in bytes, but the implementation called utf8.RuneCount, which counts Unicode code points. For multibyte content the rune count underestimates the actual JSON byte size, letting the forwarded snapshot exceed the intended budget. Change-Id: Ia080b2fe4f20825d57743c0642f8cb9c442fb858 Signed-off-by: Thomas Kosiewski <tk@coder.com>
…-prompt override and orphan tool blocks Place AdvisorSystemPrompt after inherited parent system messages so the advisor contract is the final system directive the model sees; otherwise, later parent instructions telling the model to address the end user or invoke tools would override the advisor-only contract. Drop tool-role messages whose originating assistant tool_call was truncated out of the recent window. The backward walk picks up tool results before their assistant tool_call, so a byte-budget cutoff landing between them leaves the tool_result orphaned; providers reject prompts with tool_result blocks that have no matching tool_use block. Change-Id: Id050cd8e4fb833dc413964ee11477abad5658af3 Signed-off-by: Thomas Kosiewski <tk@coder.com>
…advisor calls DEREM-31: TestAdvisorRunStripsChainStateAndIsConsistentAcrossCalls snapshotted only PreviousResponseID, so a regression that removed the Store=false assignment in resetProviderOptionsForNestedCall would have passed. Extend the observation struct to capture Store alongside PreviousResponseID and assert it is explicitly non-nil and false on every nested call. Change-Id: I008f0b24cf42b2db038d5267f6348b2756e0ae8f Signed-off-by: Thomas Kosiewski <tk@coder.com>
…isor handoff Previously, BuildAdvisorMessages forwarded all parent system messages to the advisor without a byte budget while the 12KB cap only applied to non-system messages. On chats with large parent system prompts, the nested advisor call could exceed the model's context window and surface as a provider error instead of advice. Apply a matching JSON byte budget to inherited system messages. Skip any oversized message instead of dropping the entire system preamble so smaller foundational instructions still reach the advisor. Change-Id: I324cd3a56dfec29e8279ff82d3f9134c86e13468 Signed-off-by: Thomas Kosiewski <tk@coder.com>
… truncating Walk inherited system messages newest-to-oldest when consuming the advisor system byte budget, then restore original order before appending. Previously the forward walk consumed the budget with older entries and silently dropped newer directives once the budget was exhausted, which conflicts with the nearby assumption that later system directives win. In large prompts this could cause the advisor run to miss the most recent constraints (e.g., newly injected safety or user-instruction blocks) and return guidance based on stale policy. Change-Id: I84d6f97ad4df6e9595c9bf8e34ea6f3901b9441c Signed-off-by: Thomas Kosiewski <tk@coder.com>
…ider entry Move the storeDisabled declaration inside the loop so each matching ResponsesProviderOptions entry gets its own pointer. Sharing the pointer across iterations was benign today (only one OpenAI entry, nothing writes through it afterward), but coupled future mutations together for no runtime benefit. Change-Id: I33e28c538d1bf9d92536d8f0f92b53e9cffa2c5b Signed-off-by: Thomas Kosiewski <tk@coder.com>
Each Runtime instance is scoped to a single outer chat run. Document that the MaxUsesPerRun counter never resets and that callers must construct a fresh Runtime per run, so readers do not assume the type is long-lived or has an internal reset. Change-Id: Ifa8a0fd7e1eabd4b2c52e6d7c9df7137bc49e507 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Thomas Kosiewski <tk@coder.com>
Previously, RunAdvisor consumed a use via tryAcquire before the nested
chatloop call ran. A transient provider failure or an empty model
response would still burn quota, so callers configured with
MaxUsesPerRun=1 had no chance to retry and the runtime contract
("increments on every successful advisor call") drifted from the code.
Add Runtime.release and refund the use on both error paths so the
quota is consumed only by calls that returned advice.
Change-Id: Iafe7884b5f49c0efc663317ecc831af609ed93d9
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
6d4e314 to
816b8a3
Compare

Summary
Introduce the
coderd/x/chatd/chatadvisorpackage: the self-contained runtime that performs a nested, tool-less, single-step model call to return strategic guidance to the parent agent. Also ships the thinAgentToolwrapper that the agent exposes as theadvisorbuilt-in.Motivation
The advisor is a "consult before you act" planning step. Keeping its runtime in its own package (rather than inside
chatd.goorchattool/) makes the behavior easy to unit-test, keepschattool/thin, and avoids import cycles withchatprompt.Changes
chatadvisor/types.go:AdvisorArgsandAdvisorResult(advice/limit_reached/errorvariants) plus usage metadata. Stable JSON shape for the UI.chatadvisor/guidance.go: the nested advisor system prompt andParentGuidanceBlockinjected into the outer agent (tagged<advisor-guidance>). The nested prompt explicitly tells the advisor it is advising the parent agent, must not address the user, and must not claim actions happened.chatadvisor/handoff.go: builds the advisor input from the already-prepared outer prompt tail (not a fresh DB reload) with hard truncation budgets and a recent-context bias so the advisor sees the exact context the outer model saw.chatadvisor/runner.go: wrapschatloop.Run()in strict one-step mode (Tools: nil,ProviderTools: nil,MaxSteps: 1) so the nested call is structurally incapable of calling tools.chatadvisor/tool.go: theAgentToolwrapper. Validates the question (non-empty, bounded to 2000 runes), invokes the runtime, and maps the result to a JSON tool response.Stack context
This is PR 2 of 6 in the advisor feature stack. It depends on PR 1's
ExclusiveToolNameshook; no consumer of this package exists yet (that lands in PR 4).Scope / non-goals
chatd.Validation
go test ./coderd/x/chatd/chatadvisor/...make lint📋 Implementation Plan (shared across the advisor stack)
Plan: Add a Mux-style advisor tool to coder agents/chatd
Outcome
Add a first-class
advisortool to agent chats incoderd/x/chatdthat feels native to Coder:Design decisions to lock before coding
chattool/, backed by a smallchatadvisorpackage.advisoras an exclusive/planning-only tool; mixed batches must return structured policy errors and force the model to retry cleanly.Delivery model
The work should be executed as coordinated workstreams with one integration owner and parallel contributors for low-conflict areas. The integration owner should own
coderd/x/chatd/chatd.gobecause prompt assembly, tool registration, and model resolution all converge there.Detailed workstreams
Repo evidence used for this plan
Mux reference and current chatd seams
Mux reference implementation
src/node/services/tools/advisor.ts— native advisor tool implementation.src/common/constants/advisor.ts— advisor prompt/constants and truncation policy.src/common/utils/tools/tools.ts— conditional tool registration.src/node/services/streamContextBuilder.ts— injects advisor guidance only when the tool is available.Current chatd seams
coderd/x/chatd/chatd.goprocessChat()— tool assembly, prompt assembly, and chatloop invocation.resolveChatModel()— current model/provider/key resolution seam.type Config struct— server-level chatd configuration surface.coderd/x/chatd/chatloop/chatloop.goRun()— main streaming/model loop.executeTools()— built-in tool execution/batching seam.coderd/x/chatd/chattool/— built-in tool implementations.site/src/pages/AgentsPage/components/ChatElements/tools/Tool.tsx— tool renderer dispatch.site/src/pages/AgentsPage/components/ChatConversation/messageParsing.tsandConversationTimeline.tsx— tool/result merge and rendering flow.Workstream map and ownership
coderd/x/chatd/chatd.gocoderd/x/chatd/chatadvisor/, newcoderd/x/chatd/chattool/advisor.gocoderd/x/chatd/chatloop/chatloop.go, related testsadvisor+ action-tool batches are rejected cleanly and deterministicallychatd.go,chatloop/metrics.go, optional config plumbingsite/.../tools/Tool.tsx, newAdvisorTool.tsx, storiesParallelization rules
coderd/x/chatd/chatd.goacross multiple execution agents without an integration lead. That file owns prompt building, tool registration, model resolution, and cost persistence.coder/muxinto a temporary directory (for example,$(mktemp -d)/mux) and inspect it read-only; do not vendor or copy code from Mux directly.Phase 0 — Preflight and guardrails
Goals
Tasks
Confirm the MVP boundary.
Confirm local workflow hygiene before coding.
scripts/githooks.--no-verify../scripts/develop.shfor the full dev server rather than manual build/run commands.Lock the model-selection policy.
AdvisorModelConfigID-style override that resolves through the existingconfigCache/model-config path. Do not invent a new free-formprovider:modelparser if chatd already stores provider/model separately.Lock the persistence policy.
queries/*.sql+make genworkflow.Create an execution ADR note in the work item or tracking doc.
Quality gate
Phase 1 — Build the advisor runtime and tool wrapper
Goals
Create the core advisor implementation in a way that is easy to test and keeps
chattool/thin.Files to add
coderd/x/chatd/chatadvisor/types.gocoderd/x/chatd/chatadvisor/guidance.gocoderd/x/chatd/chatadvisor/handoff.gocoderd/x/chatd/chatadvisor/runtime.gocoderd/x/chatd/chatadvisor/runner.gocoderd/x/chatd/chattool/advisor.goResponsibilities by file
types.goadvicelimit_reachederrorRecommended shape:
guidance.goruntime.goMaxUsesPerRun;MaxOutputTokens;handoff.gorunner.gochatloop.Run()in an in-memory, one-step mode:Tools: nilProviderTools: nilMaxSteps: 1PersistStep: capture the assistant output in memory instead of writing DB rowschattool/advisor.goAdvisorArgs;Questionis non-empty and bounded;chatadvisorrunner;Defensive programming requirements
Questionis non-empty after trimming.AdvisorResult.Typeis one of the known variants before returning.Acceptance criteria
adviceresult.chatadvisor/, not embedded insidechatd.go.Phase 2 — Wire advisor into chatd and keep prompt/tool availability in sync
Goals
Register the tool in the right place, expose it only when eligible, and inject system guidance only when the tool is present.
Files to modify
coderd/x/chatd/chatd.gochatd.gobecomes too crowdedTasks
Compute one eligibility boolean in
processChat().Recommended inputs:
chat.ParentChatID == uuid.Nilor equivalent existing root/child check);Create the runtime once per outer chat run.
resolveChatModel().ChatModelCallConfig.MaxUsesPerRunandMaxOutputTokensfrom advisor config defaults.Register the tool in the built-in tool block.
processChat().builtinToolNames["advisor"] = trueso metrics stay bounded.Inject advisor guidance into the outer system prompt using the same boolean.
chatprompt.InsertSystem()in the same prompt assembly path that already injects user/system instructions.<advisor-guidance>so it is easy to spot in tests and future refactors.Keep advisor out of child chats for the first release.
spawn_agent/wait_agentflows.Acceptance criteria
advisorso metrics do not collapse it into the genericmcplabel.Phase 3 — Enforce planning-only execution policy in
chatloopGoals
Prevent the model from calling
advisorand action tools in the same execution batch.Files to modify
coderd/x/chatd/chatloop/chatloop.goRecommended implementation
Keep the MVP small; do not build a general policy engine yet.
Add a minimal field to
chatloop.RunOptions, for example:In
Run()/executeTools(), detect the case where the exclusive tool appears in the same local-tool batch as any other locally executed tool.When that happens, synthesize structured tool-result errors for the affected calls instead of executing anything in the batch.
advisorshould receive a clear error like: advisor must be called by itself before action tools.Let the outer model see those tool errors and retry cleanly.
Pass the just-finished step snapshot into the tool execution context.
Why this is the right fit
Acceptance criteria
advisorsucceeds.advisorplus any other locally executed tool returns deterministic policy errors and executes nothing.Phase 4 — Usage limits, metrics, and configuration
Goals
Make advisor safe to operate without over-designing billing/storage in the first release.
Files to modify
coderd/x/chatd/chatd.gocoderd/x/chatd/chatloop/metrics.goas neededcoderd/x/chatd/chatd.goConfigstruct and constructor pathTasks
Add explicit server config knobs for MVP.
Recommended fields on
chatd.Configor a nested advisor config struct:AdvisorEnabled boolAdvisorMaxUsesPerRun intAdvisorMaxOutputTokens int64Track usage per outer run.
processChat()invocation.remaining_usesin the tool result.limit_reachedwhen the cap is exhausted.Expose advisor usage metadata in the tool result.
callConfig.Costcalculation path as the outer chat for MVP if advisor reuses the same model.Record server-side metrics.
advisor.Optional decision gate: separate advisor model.
configCachepath.Optional decision gate: queryable advisor cost.
coderd/database/queries/*.sql;make gen;Acceptance criteria
Phase 5 — Frontend rendering and Storybook coverage
Goals
Make advisor feel intentional in the Agents UI without blocking the backend on fancy streaming UI.
Files to modify
site/src/pages/AgentsPage/components/ChatElements/tools/Tool.tsxsite/src/pages/AgentsPage/components/ChatElements/tools/AdvisorTool.tsxDelivery strategy
Intermediate milestone during backend bring-up: rely on the existing generic tool renderer if needed.
Release milestone: add a dedicated lightweight
AdvisorToolrenderer.ToolCollapsibleToolIconResponsefor markdown/prose renderingScrollAreaif the advice can be longsite/src/pages/AgentsPage/; that area is already React-Compiler aware.Render the structured result states cleanly.
advice— readable prose/markdown with optional metadata footer.limit_reached— warning-style message.error— error state with visible fallback text.running— existing tool loading state/spinner is enough for MVP.Add Storybook coverage instead of ad-hoc component tests.
Recommended stories:
Keep the UI contract narrow.
adviceplus small metadata rather than a deeply nested schema.Acceptance criteria
Phase 6 — Automated tests and validation gates
Backend tests to add
Advisor runtime/tool tests
Prompt/gating tests in chatd
Chatloop policy tests
Usage/metrics tests
advisor;Frontend tests to add
play()assertions for the advisor renderer states.Recommended command sequence
Run these as the implementation matures, not only at the end:
Backend-focused gate after phases 1–4:
make test RUN=TestAdvisormake test RUN=TestChatloopAdvisormake lintFrontend-focused gate after phase 5:
pnpm test:storybook src/pages/AgentsPage/components/ChatElements/tools/AdvisorTool.stories.tsxpnpm lintpnpm formatFinal repo gate before handoff:
make pre-commitmake test RUN=...selections covering touched chatd pathsDogfooding plan
Principle
Dogfood the change as a real agent feature, not just a unit-tested backend. Per the dogfood and
agent-browserskills, the reviewer should get watchable repro videos plus screenshots that make the behavior obvious without reading logs.Required setup
./scripts/develop.shsite/with:pnpm storybook --no-openagent-browserdirectly — nevernpx agent-browser../dogfood-output/advisor/screenshots/andvideos/Evidence protocol
For every interactive scenario below:
For static UI states (for example Storybook error/limit cards), an annotated screenshot is sufficient; video is optional but still encouraged by this project’s review preference.
Dogfood scenarios
Scenario A — Happy path in the real Agents UI
Goal: prove that a root agent chat can invoke advisor and produce a readable recommendation before taking further action.
Steps:
Pass criteria:
Scenario B — Advisor unavailable path
Goal: prove the feature is truly gated.
Suggested variants (at least one is required, both are better):
Evidence:
Pass criteria:
Scenario C — UI states in Storybook
Goal: prove the renderer handles non-happy states cleanly.
Required story states:
Evidence:
Pass criteria:
Scenario D — Regression sweep of nearby tools
Goal: ensure advisor does not break the surrounding chat timeline.
Check at minimum:
Evidence:
agent-browserusage notes for the QA agentagent-browser batchfor 2+ sequential commands when no intermediate parsing is needed.snapshot -ito discover interactive refs.wait --load networkidleunless the page is known to go idle; prefer explicit element/text waits or short fixed waits.Rollout plan
Initial rollout
Expansion conditions
Expand beyond the initial rollout only after the following are true:
Explicit non-goals for the first release
Final acceptance checklist
advisoris a built-in chatd tool, not an MCP/dynamic-tool substitute.make lint, targetedmake test, Storybook tests,make pre-commit) passed before handoff.Suggested PR split
PR 1 — Backend foundation
chatadvisor/packagechattool/advisor.gochatloopexclusive policyPR 2 — Frontend + QA
PR 3 — Optional follow-ups only if demanded by stakeholders
Generated with
mux• Model:anthropic:claude-opus-4-7• Thinking:max