Skip to content

CON-112: Campaign Assistant — per-campaign chat over content_plan + enrich_brief - #68

Merged
grsmv merged 26 commits into
mainfrom
feature/con-112-campaign-assistant
Jul 17, 2026
Merged

CON-112: Campaign Assistant — per-campaign chat over content_plan + enrich_brief#68
grsmv merged 26 commits into
mainfrom
feature/con-112-campaign-assistant

Conversation

@grsmv

@grsmv grsmv commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Adds a conversational, tenant-scoped Campaign Assistant that mirrors the Post Assistant architecture (shared-service + tool + REST + SSE). A cheap planning model (new RolePlanning / PLANNING_MODEL_ID, Haiku) drives intent-routing while the existing content_plan and enrich_brief flows are invoked as tools for the heavy prose. enrichBrief auto-applies the four brief fields to the campaign.

  • migration + TenantScoped model + repository for campaign_assistant_messages
  • genkit/flows/campaign_assistant: types, scanner, context, flow, tools, run, prompt
  • llm.RolePlanning + config.PlanningModelID; NewProvider gains a planning model
  • server wiring (genkit_runtime + initCampaignAssistant) behind the Anthropic gate
  • POST /api/campaigns/:id/assistant (SSE) + GET /api/campaigns/:id/messages, tenant-wide (no owner guard), diverging intentionally from the owner-only standalone generate-draft/enrich-brief endpoints
  • planner usage recorded under campaign_assistant; sub-flows keep their own
  • updated NewCampaignsHandler call sites across tests

Summary by CodeRabbit

  • New Features
    • Added Campaign Assistant SSE workflow (Q&A, brief enrichment, content planning, targeted post generation, date changes, post redistribution) plus read-only brief/posts consistency checks.
    • Added per-campaign assistant message persistence and a “recent messages” endpoint.
    • Added campaign overview snapshots to support the assistant workflow and quick inspection.
  • Diagnostics
    • Added optional pprof support and Anthropic HTTP request/trace debugging.
    • Added configurable planning model and generation/consistency limits.
  • Bug Fixes
    • Ensured “empty history” endpoints consistently return empty arrays.
  • Tests
    • Expanded unit, HTTP/SSE, and integration coverage for the new assistant and review flows.

…nrich_brief

Adds a conversational, tenant-scoped Campaign Assistant that mirrors the Post
Assistant architecture (shared-service + tool + REST + SSE). A cheap planning
model (new RolePlanning / PLANNING_MODEL_ID, Haiku) drives intent-routing while
the existing content_plan and enrich_brief flows are invoked as tools for the
heavy prose. enrichBrief auto-applies the four brief fields to the campaign.

- migration + TenantScoped model + repository for campaign_assistant_messages
- genkit/flows/campaign_assistant: types, scanner, context, flow, tools, run, prompt
- llm.RolePlanning + config.PlanningModelID; NewProvider gains a planning model
- server wiring (genkit_runtime + initCampaignAssistant) behind the Anthropic gate
- POST /api/campaigns/:id/assistant (SSE) + GET /api/campaigns/:id/messages,
  tenant-wide (no owner guard), diverging intentionally from the owner-only
  standalone generate-draft/enrich-brief endpoints
- planner usage recorded under campaign_assistant; sub-flows keep their own
- updated NewCampaignsHandler call sites across tests
@linear-code

linear-code Bot commented Jul 14, 2026

Copy link
Copy Markdown

CON-112

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 72b08b59-238c-412a-8c09-970091a00c91

📥 Commits

Reviewing files that changed from the base of the PR and between 85698a3 and 5667017.

📒 Files selected for processing (2)
  • src/genkit/flows/campaign_assistant/tools.go
  • src/genkit/flows/campaign_assistant/tools_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/genkit/flows/campaign_assistant/tools_test.go
  • src/genkit/flows/campaign_assistant/tools.go

Walkthrough

Adds a tenant-scoped Campaign Assistant with streamed Genkit interactions, targeted post generation, consistency reviews, campaign overview data, conversation persistence, HTTP endpoints, model routing, diagnostics, and deterministic campaign date redistribution.

Changes

Campaign Assistant

Layer / File(s) Summary
Contracts and persistence
src/genkit/flows/campaign_assistant/types.go, src/models/*, src/repository/*, src/database/migrations/*
Defines assistant responses, SSE payloads, tenant-scoped message storage, chronological history reads, atomic turn persistence, and scheduled-date batch updates.
Assistant execution
src/genkit/flows/campaign_assistant/{flow,context,tools,run}.go, src/genkit/flows/campaign_assistant/prompts/*
Registers the flow, renders campaign context, invokes tools, streams events, reconstructs responses, and persists turns.
Campaign actions and generation
src/genkit/flows/content_plan/*, src/campaign_actions/{overview,reschedule}/*
Adds targeted post generation, deterministic overview aggregation, phase-aware redistribution, and effective-window validation.
Consistency reviews
src/genkit/flows/consistency/*, src/server/consistency.go
Adds brief and post review flows with prompts, structured findings, caps, filtering, and SSE callbacks.
HTTP and runtime wiring
src/handlers/campaigns.go, src/server/*, src/vendors/llm/*
Exposes assistant-related endpoints and wires repositories, callbacks, planning-model routing, availability gates, and server diagnostics.
Shared streaming and validation
src/genkit/jsonstream/*, src/genkit/flows/{enrich_brief,post_assistant}/run.go, src/*_test.go, http-client/*
Moves JSON scanning into a shared package and adds endpoint, repository, flow, integration, and HTTP-client coverage.

Prototype cleanup

Layer / File(s) Summary
Remove obsolete prototype modules
prototyping/CON-28-campaign-content-plan-generation/*, prototyping/CON-42-post-assistant/*, prototyping/CON-55-event-stream/*
Removes prototype manifests, UI components, streaming helpers, entry points, styles, and frontend configuration.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • ogen-app/ogen#40: Updates the shared JSON streaming scanner used by the enrich-brief flow.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.37% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main feature: a per-campaign Campaign Assistant chat over content_plan and enrich_brief.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/con-112-campaign-assistant

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/vendors/llm/llm_test.go (1)

99-109: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Quality and planning use the same model string — arg-order swap bugs would go undetected.

Use a distinct literal for the planning model so a constructor arg-order regression (quality ↔ planning) actually fails the test.

🧪 Proposed fix
-	p := llm.NewProvider("claude-sonnet-4-5-20250929", "claude-haiku-4-5-20251001", "claude-haiku-4-5-20251001")
+	p := llm.NewProvider("claude-sonnet-4-5-20250929", "claude-haiku-4-5-20251001", "claude-haiku-3-5-20241022")
 	if got := p.Ref(llm.RoleGeneration); got != "anthropic/claude-sonnet-4-5-20250929" {
 		t.Errorf("generation Ref = %q", got)
 	}
 	if got := p.Model(llm.RoleQuality); got != "claude-haiku-4-5-20251001" {
 		t.Errorf("quality Model = %q", got)
 	}
-	if got := p.Ref(llm.RolePlanning); got != "anthropic/claude-haiku-4-5-20251001" {
+	if got := p.Ref(llm.RolePlanning); got != "anthropic/claude-haiku-3-5-20241022" {
 		t.Errorf("planning Ref = %q", got)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vendors/llm/llm_test.go` around lines 99 - 109, Update TestProviderRef to
pass a distinct model literal for the planning argument in llm.NewProvider, then
update the RolePlanning expectation to that distinct value while keeping the
quality model assertion unchanged. This must make quality/planning constructor
argument swaps fail the test.
src/genkit/flows/campaign_assistant/scanner.go (1)

253-263: 🎯 Functional Correctness | 🔵 Trivial

Nested-value skip doesn't track string state — a brace inside a nested string can end the skip early.

stInNested counts {/[/}/] unconditionally. A quoted string inside a skipped nested value containing a literal }/] (e.g. {"note": "a } inside"}) decrements nestedDepth prematurely, exiting the skip mid-value and letting the trailing text be mis-parsed as top-level content.

Not triggered by the current envelope (no nested keys), but the type's docstring claims general tolerance for arbitrary/malformed model output, and this is the shared parser other future response shapes might rely on.

♻️ Sketch: track quote state while skipping nested structures
 case stInNested:
+    if s.nestedInString {
+        if s.esc == escBackslash {
+            s.esc = escNone
+        } else if c == '\\' {
+            s.esc = escBackslash
+        } else if c == '"' {
+            s.nestedInString = false
+        }
+        return
+    }
     switch c {
+    case '"':
+        s.nestedInString = true
     case '{', '[':
         s.nestedDepth++
     case '}', ']':
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/genkit/flows/campaign_assistant/scanner.go` around lines 253 - 263,
Update the stInNested handling in the scanner state machine to track whether it
is inside a quoted string, ignoring braces and brackets while quoted and
respecting escaped quotes. Only adjust nestedDepth and return to stTop when
structural delimiters are encountered outside strings, preserving existing
resetKey behavior.
src/genkit/flows/campaign_assistant/tools.go (1)

157-157: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid manual struct literal when underlying types match.

EnrichBriefInput and EnrichBriefStartedEventPayload share identical fields. Cast directly.

♻️ Proposed refactor
-	emit(st.onEvent, SSEEventEnrichBriefStarted, EnrichBriefStartedEventPayload{Instruction: in.Instruction})
+	emit(st.onEvent, SSEEventEnrichBriefStarted, EnrichBriefStartedEventPayload(in))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/genkit/flows/campaign_assistant/tools.go` at line 157, Update the emit
call in the EnrichBrief flow to cast the existing EnrichBriefInput value
directly to EnrichBriefStartedEventPayload instead of constructing a manual
struct literal, preserving the current event payload contents.

Source: Linters/SAST tools

src/handlers/campaigns_test.go (1)

792-975: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test coverage for the new POST /:id/assistant and GET /:id/messages endpoints.

Every other campaign endpoint (including EnrichBrief right above) has a dedicated Describe block covering the 503-unwired, 404-unknown-campaign, success-stream, and error-stream paths. The new Assistant/ListMessages endpoints have none in this file — the buildBriefApp/parseSSE helpers here are directly reusable as a template.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/handlers/campaigns_test.go` around lines 792 - 975, Add dedicated test
coverage in campaigns_test.go for POST /:id/assistant and GET /:id/messages,
following the existing EnrichBrief Describe block and reusing buildBriefApp,
seedCookie, createCampaignOn, and parseSSE where applicable. Cover unwired 503
behavior, unknown-campaign 404 responses, successful assistant/message streaming
or responses, and error-stream handling consistent with each endpoint’s
contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/genkit/flows/campaign_assistant/run.go`:
- Around line 237-260: Update the conversation persistence flow around
persistTurn so a history-write failure is logged and does not abort the
assistant turn or suppress subsequent completion events. Within persistTurn,
wrap both repos.Messages.Create calls in a single transaction so the user and
model messages commit or roll back together.

In `@src/handlers/campaigns.go`:
- Around line 584-593: Normalize the msgs result in
CampaignsHandler.ListMessages before calling c.JSON so a nil or empty
ListRecentByCampaignID result is returned as an empty
[]models.CampaignAssistantMessage array, preserving the endpoint’s array
response contract.

---

Nitpick comments:
In `@src/genkit/flows/campaign_assistant/scanner.go`:
- Around line 253-263: Update the stInNested handling in the scanner state
machine to track whether it is inside a quoted string, ignoring braces and
brackets while quoted and respecting escaped quotes. Only adjust nestedDepth and
return to stTop when structural delimiters are encountered outside strings,
preserving existing resetKey behavior.

In `@src/genkit/flows/campaign_assistant/tools.go`:
- Line 157: Update the emit call in the EnrichBrief flow to cast the existing
EnrichBriefInput value directly to EnrichBriefStartedEventPayload instead of
constructing a manual struct literal, preserving the current event payload
contents.

In `@src/handlers/campaigns_test.go`:
- Around line 792-975: Add dedicated test coverage in campaigns_test.go for POST
/:id/assistant and GET /:id/messages, following the existing EnrichBrief
Describe block and reusing buildBriefApp, seedCookie, createCampaignOn, and
parseSSE where applicable. Cover unwired 503 behavior, unknown-campaign 404
responses, successful assistant/message streaming or responses, and error-stream
handling consistent with each endpoint’s contract.

In `@src/vendors/llm/llm_test.go`:
- Around line 99-109: Update TestProviderRef to pass a distinct model literal
for the planning argument in llm.NewProvider, then update the RolePlanning
expectation to that distinct value while keeping the quality model assertion
unchanged. This must make quality/planning constructor argument swaps fail the
test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7ffa07ab-09bb-4aa9-ae6b-0b2ab849d7ad

📥 Commits

Reviewing files that changed from the base of the PR and between 7e518f2 and 93cd0d2.

📒 Files selected for processing (28)
  • src/config/config.go
  • src/database/migrations/20260714000001_campaign_assistant_messages.down.sql
  • src/database/migrations/20260714000001_campaign_assistant_messages.up.sql
  • src/genkit/flows/campaign_assistant/context.go
  • src/genkit/flows/campaign_assistant/flow.go
  • src/genkit/flows/campaign_assistant/prompts/campaign_assistant.tmpl
  • src/genkit/flows/campaign_assistant/run.go
  • src/genkit/flows/campaign_assistant/scanner.go
  • src/genkit/flows/campaign_assistant/tools.go
  • src/genkit/flows/campaign_assistant/types.go
  • src/handlers/analytics_test.go
  • src/handlers/campaigns.go
  • src/handlers/campaigns_test.go
  • src/handlers/multi_tenant_test.go
  • src/handlers/post_attachments_test.go
  • src/handlers/posts_test.go
  • src/integration/post_analytics_test.go
  • src/integration/post_attachments_test.go
  • src/integration/post_clone_test.go
  • src/integration/post_restore_test.go
  • src/integration/post_schedule_test.go
  • src/models/campaign_assistant_message.go
  • src/repository/campaign_assistant_messages.go
  • src/server/campaign_assistant.go
  • src/server/genkit_runtime.go
  • src/server/server.go
  • src/vendors/llm/llm_test.go
  • src/vendors/llm/provider.go

Comment thread src/genkit/flows/campaign_assistant/run.go
Comment thread src/handlers/campaigns.go
- repository/campaign_assistant_messages_test.go: ordering, limit, tenant isolation
- handlers/campaigns_test.go: assistant SSE (401/503/400/deltas+complete/tool-forward/
  error 502) and messages (401/empty/chronological), 9 specs
- integration/campaign_assistant_test.go: real-flow suite (integration tag) wiring all
  three flows — Q&A (no mutation), enrich (persisted), content plan (persisted), history
- http-client/campaigns/campaigns.http: campaign assistant + messages requests
- run.go: a persistTurn failure is now logged and no longer aborts the turn or
  suppresses the completion events — tool side effects (posts/brief) have already
  committed, so the turn is still reported successful.
- persistTurn writes the user + model messages via a new
  CampaignAssistantMessageRepository.CreateBatch, which runs both inserts in one
  transaction (all-or-nothing) so a turn never persists half-written.
ListRecentByCampaignID scans into a nil slice on an empty result set, and
c.JSON(nil) serializes to null, breaking the array response contract. Normalize
to an empty slice before encoding, matching the messageRepo==nil branch.
Tighten the empty-history test to assert the raw body is [] (decoding null into
a slice also yields empty, so the old assertion was blind to this).
Mirror the campaigns fix: ListRecentByPostID scans into a nil slice on an empty
result set, and c.JSON(nil) serializes to null, breaking the array contract.
Normalize to an empty slice before encoding.
New campaignoverview shared service (pure buildOverview aggregation + repo-backed
Overview) reused by two surfaces:
- getCampaignOverview assistant read tool (campaign_assistant flow) — phases +
  per-phase post counts + distribution by status/platform/content-type
- GET /api/campaigns/:id/overview REST endpoint (JSON; 404 on not-found), wired
  via CampaignsHandler.SetOverviewService (setter, no constructor churn)

Phases are also added to the assistant's always-on context block (free — already
hydrated on the campaign), so phase questions need no tool round-trip; the
per-turn posts query for distribution stays on-demand via the tool/endpoint.

Tests: pure buildOverview unit tests (buckets, unassigned/none, reconciling
totals, empty/no-type); handler endpoint specs (200 shape / 401 / 404 / 503);
integration spec for the tool. http-client entry added.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/genkit/flows/campaign_assistant/scanner_test.go (1)

1-415: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the scanner test copies identical. This file matches enrich_brief and post_assistant, but it drifts from content_plan; update all four copies together, with only the package line allowed to differ.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/genkit/flows/campaign_assistant/scanner_test.go` around lines 1 - 415,
Synchronize the complete scanner test contents across the campaign_assistant,
enrich_brief, post_assistant, and content_plan copies so they are identical,
allowing only each package declaration to differ. Apply any future test changes
consistently to all four files and verify no other differences remain.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/genkit/flows/campaign_assistant/scanner_test.go`:
- Around line 1-415: Synchronize the complete scanner test contents across the
campaign_assistant, enrich_brief, post_assistant, and content_plan copies so
they are identical, allowing only each package declaration to differ. Apply any
future test changes consistently to all four files and verify no other
differences remain.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 74ec94fe-75e2-49b7-a857-bd7fd8967440

📥 Commits

Reviewing files that changed from the base of the PR and between 93cd0d2 and f237fcf.

📒 Files selected for processing (19)
  • http-client/campaigns/campaigns.http
  • src/campaignoverview/overview.go
  • src/campaignoverview/service.go
  • src/campaignoverview/service_test.go
  • src/genkit/flows/campaign_assistant/context.go
  • src/genkit/flows/campaign_assistant/prompts/campaign_assistant.tmpl
  • src/genkit/flows/campaign_assistant/run.go
  • src/genkit/flows/campaign_assistant/scanner_test.go
  • src/genkit/flows/campaign_assistant/tools.go
  • src/genkit/flows/campaign_assistant/types.go
  • src/handlers/campaigns.go
  • src/handlers/campaigns_test.go
  • src/handlers/posts.go
  • src/integration/campaign_assistant_test.go
  • src/repository/campaign_assistant_messages.go
  • src/repository/campaign_assistant_messages_test.go
  • src/server/campaign_assistant.go
  • src/server/genkit_runtime.go
  • src/server/server.go
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/server/campaign_assistant.go
  • src/genkit/flows/campaign_assistant/prompts/campaign_assistant.tmpl
  • src/genkit/flows/campaign_assistant/context.go
  • src/server/genkit_runtime.go
  • src/genkit/flows/campaign_assistant/tools.go
  • src/server/server.go
  • src/genkit/flows/campaign_assistant/run.go
  • src/genkit/flows/campaign_assistant/types.go
  • src/handlers/campaigns.go

Mirror the post_actions/{clone,restore,schedule} layout: relocate the shared
service to src/campaign_actions/overview and rename the package
campaignoverview -> overview. Pure move + rename; no behaviour change.
post_assistant, enrich_brief, and campaign_assistant each carried a byte-identical
471-line scanner.go (+ identical scanner_test.go). Extract the one copy to a shared
src/genkit/jsonstream package (type JSONStringScanner -> jsonstream.Scanner,
NewJSONStringScanner -> jsonstream.New) and delete the two duplicates; the three
run.go call sites now use jsonstream.New. content_plan's separate jsonPostScanner
is unaffected. Pure de-duplication, no behaviour change.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/server/anthropic_tool_order.go`:
- Around line 56-57: Update the request handling around readAnthropicReqBody to
clone req before any mutation, and perform subsequent body, ContentLength, or
GetBody changes on the cloned request. Keep the original request untouched while
preserving the existing request-processing behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 827560ed-83db-41a6-b7bc-42df07cae2f1

📥 Commits

Reviewing files that changed from the base of the PR and between 8389fcc and 2788156.

📒 Files selected for processing (9)
  • src/config/config.go
  • src/genkit/flows/campaign_assistant/flow.go
  • src/genkit/flows/campaign_assistant/prewarm.go
  • src/genkit/flows/campaign_assistant/types.go
  • src/server/anthropic_http_debug.go
  • src/server/anthropic_tool_order.go
  • src/server/anthropic_tool_order_test.go
  • src/server/campaign_assistant.go
  • src/server/genkit_runtime.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/genkit/flows/campaign_assistant/flow.go
  • src/server/campaign_assistant.go
  • src/server/anthropic_http_debug.go
  • src/config/config.go
  • src/server/genkit_runtime.go
  • src/genkit/flows/campaign_assistant/types.go

Comment thread src/server/anthropic_tool_order.go
post_assistant sends 7 strict tools, so it has the same cold-compile cost as
campaign_assistant: the global tool-order stabilizer keeps its cache warm after
the first request, but that first request still paid the ~50s grammar compile.

Mirror the campaign_assistant pre-warm: a PrewarmTools flag on the flow config
(set by the server from AnthropicStableToolOrder) fires one throwaway
max_tokens:1 generation carrying the full 7-tool set in the background at flow
init, under the generation model the flow actually uses. WithMaxTurns(1) +
max_tokens:1 means no tool executes during warm-up.
grsmv added 6 commits July 16, 2026 12:21
… skill

Capture the hard-won lesson so the next tool-using flow doesn't re-suffer it:
- New Gotcha #15 — root cause (Genkit map-ordered tools + forced strict:true →
  Anthropic recompiles the constrained-decoding grammar per random tool-set,
  ~50s/request), the global stabilizer fix, the per-flow pre-warm pattern, and
  the diagnosis technique (ANTHROPIC_DEBUG_HTTP server_ms vs conn_ms; diff the
  outgoing bytes — never trust wall-clock alone).
- A warning callout in Step 5 (Tools) pointing to Gotcha #15.
- A checklist item for wiring the cold-start pre-warm on new tool flows.
toolSetCampaignDates persisted the new dates before counting out-of-range
posts, and swallowed a Posts.ListByCampaign error (err == nil guard) — a failed
lookup silently reported PostsOutsideRange: 0 while the dates were already
saved.

Load posts before Campaigns.Update, return the repository error instead of
treating a failed lookup as zero, and compute PostsOutsideRange from the loaded
posts before persisting. A lookup failure now aborts cleanly with no partial
side effect; the count is unchanged on the happy path since Update doesn't touch
posts' ScheduledAt.
The posts-consistency limit used a positive req.Max directly, so a model-supplied
max bypassed cfg.MaxPosts — the per-call cap that bounds review cost.

Compute an effective cap (cfg.MaxPosts when positive, else defaultMaxPosts),
start the limit at that cap, and lower it only when a positive req.Max is
smaller. A caller may request fewer posts but can never exceed the cap; the
defaultMaxPosts fallback is preserved.
UpdateScheduledAtBatch blind-wrote scheduled_at by PK+tenant with no status
guard, so a post concurrently published or scheduled between the redistribute
read and this write would have its schedule clobbered.

Restrict each update to status IN (draft, ready_for_publish) — the reschedule
eligibility set — and return an error unless exactly one row is affected. A post
that was redistributed or is no longer eligible matches zero rows and fails the
whole transaction, rolling back rather than applying a stale plan.
The pass-through test left the RoundTrip response body unclosed (a bodyclose
omission). Close it after the successful call, before the assertions; behavior
is unchanged since the body is a NopCloser.
anthropicToolOrderTransport.RoundTrip rewrote the sorted body directly on the
caller's request (setAnthropicReqBody plus readAnthropicReqBody's body-consuming
fallback), violating the http.RoundTripper contract that RoundTrip must not
modify the request.

Clone the request and apply the Body/ContentLength/GetBody changes to the clone,
forwarding the clone and leaving the original untouched. readAnthropicReqBody now
reads a copy via GetBody (which net/http sets for the SDK's in-memory payload)
without consuming or resetting the original body; it forwards untouched when
GetBody is unavailable.

Add a RoundTrip test asserting the base receives a sorted clone while the
original request's GetBody still yields the original tool order.
@grsmv

grsmv commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

resolveWindow only applied the default window when both windowStart and
windowEnd were empty, so when the planner resolved a vague timeframe like
"upcoming weeks" into just a start (no end), it tried to parse the empty end and
hard-failed with "windowEnd must be an ISO date" — aborting the whole
campaign_assistant turn.

Derive the missing bound instead: start-only -> end = start + 14 days;
end-only -> start = today. The both-empty default, past-start clamp, and
end-before-start guard are unchanged, and a non-empty malformed date is still
rejected. Extend TestResolveWindow with the single-bound cases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant