Skip to content

CON-128: hybrid Post Assistant — cheap Haiku planner + Sonnet editPost writer - #110

Merged
grsmv merged 8 commits into
mainfrom
feature/con-128-post-assistant-hybrid-models
Aug 12, 2026
Merged

CON-128: hybrid Post Assistant — cheap Haiku planner + Sonnet editPost writer#110
grsmv merged 8 commits into
mainfrom
feature/con-128-post-assistant-hybrid-models

Conversation

@grsmv

@grsmv grsmv commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

The Post Assistant ran its whole turn on one Sonnet call that did two jobs at once: routing the tools (clone/restore/schedule/note/asset-lookup/Q&A) and writing the edited copy inline. This PR splits them — the orchestration loop now runs on the cheap/fast planning model (Haiku, RolePlanning) and delegates all copywriting to a Sonnet (RoleGeneration) editPost write-tool — mirroring the Campaign Assistant (CON-112). Routine turns become Haiku-only; edit turns pay Haiku for routing plus a lean Sonnet call for prose (no 8-tool grammar), so both turn types get cheaper with no change to the model that actually writes.

Backend-only. The external SSE/REST contract is unchanged.

What changed

  • editPost write-tool (tools.go) — runs a nested Sonnet generation, streams content_delta, and returns only a compact receipt ({ok, chars}) to the planner. The full content flows via SSE + requestState, never back through the cheap model, so Haiku can't mangle (or re-bill) the copy.
  • Shared writer backs both editPost and clonePost cross-platform adaptation; verbatim/same-platform clone, restore, schedule, note, asset retrieval, and Q&A stay Haiku-only.
  • run.go branches on PlannerEnabled: loop model, output cap, scanner watch-list (explanation-only vs +updatedContent), tool set (+editPost), metering model, and a new editResult authoritative branch (placed before note-handling so edit-and-note turns stay edited).
  • Prompt split — new planner block (routes, delegates writing) and writer block (Markdown copywriter). The original system block is left untouched as the legacy fallback.
  • Metering — planner recorded as post_assistant (Haiku), writer as post_assistant_edit (Sonnet), for clean per-model attribution; no double count.
  • Prewarm now warms the planner tool set (incl. editPost) on RolePlanning.
  • Safety guard — if the planner claims action: "edited" without invoking editPost, the runner never persists an empty body (it would wipe the post); the turn downgrades to noted/declined and drops the version snapshot. The legacy path can't hit this (content precedes action in one JSON envelope).

Config & rollout

  • POST_ASSISTANT_PLANNER (default true) — the kill-switch. Set false to revert the whole assistant to the proven single-Sonnet path.
  • POST_ASSISTANT_PLANNER_MAX_OUTPUT_TOKENS (default 8192) — planner envelope cap; the writer keeps MAX_OUTPUT_TOKENS (64000).
  • Model ids stay tunable via MODEL_ID / PLANNING_MODEL_ID.

Backward compatibility

  • SSE event set and payloads are unchanged — content_delta simply originates from the writer sub-call now.
  • POST /api/posts/:id/assistant request/response shapes unchanged.
  • Legacy single-Sonnet path retained behind the flag (removal is a future cleanup once the hybrid proves out).

Testing

  • go build ./..., go vet ./..., and post_assistant package tests green.
  • ✅ New guard unit tests for editPost / runWriter.
  • ✅ Integration test fixed (constructed a real Provider — it previously built the flow config with a nil Provider and would panic) and extended: exercises the hybrid path by default (togglable to legacy via POST_ASSISTANT_PLANNER=false), and asserts content_delta streams on an edit — proving the editPost → writer → client wiring end-to-end. Compiles with -tags integration.
  • Pending (needs a live Anthropic key, reviewer/author env):
    • Run the integration suite on both paths: go test -tags integration ./src/integration/...
    • Before/after cost + quality eval (PRD §12) — the acceptance guardrail: routing accuracy, edit-quality parity, per-turn cost, and TTFT to first content_delta.

Risk

  • Extra Haiku routing hop before content streams (Haiku TTFT is low + prewarmed).
  • Haiku routing quality — mitigated by the planner prompt, the empty-content guard, and existing prose/recovery nets. Precedent that Haiku can underdeliver (QUALITY_MODEL_ID stayed Sonnet for post-quality) is exactly why the kill-switch + eval exist.

Related

CON-112 (Campaign Assistant — the hybrid precedent this mirrors), CON-86 (role-based Provider + metering), CON-59 (clonePost shared-service pattern), CON-85 (post-quality Haiku-underdelivered precedent).

Summary by CodeRabbit

  • New Features

    • Added optional hybrid planning mode to separate request planning from post writing.
    • Added guided post editing with streamed content updates.
    • Improved cross-platform cloning with automatic content adaptation.
    • Added configurable planner output limits and separate planning and writing models.
    • Added support for using retrieved source excerpts in generated edits.
  • Bug Fixes

    • Prevented incomplete or empty edits from overwriting existing content.
  • Tests

    • Expanded coverage for editing, writing availability, excerpt handling, and streamed results.

grsmv and others added 3 commits August 12, 2026 13:51
… write-tool

Split the Post Assistant's single Sonnet call — which both routed tools and
wrote the edited copy — into a cheap Haiku orchestration loop (RolePlanning)
that delegates all copywriting to a Sonnet (RoleGeneration) editPost
write-tool, mirroring the Campaign Assistant (CON-112).

- editPost tool runs a nested Sonnet generation, streams content_delta, and
  returns only a compact receipt to the planner; the content flows via SSE +
  requestState so the cheap planner stays cheap and can't mangle the copy.
- clonePost cross-platform adaptation now routes through the same writer.
- run.go branches on PlannerEnabled: loop model, output cap, scanner watch
  list, tool set (+editPost), metering model, and a new editResult branch
  (before note-handling so edit-and-note turns stay 'edited').
- Writer usage metered under post_assistant_edit for per-model attribution.
- Prompt split into planner + writer blocks; the legacy 'system' block is left
  untouched as the POST_ASSISTANT_PLANNER=false rollback path.
- Prewarm targets the planner tool set on RolePlanning.

External SSE/REST contract unchanged. Default on; kill-switch reverts to the
proven single-Sonnet path. Adds guard unit tests for editPost/runWriter.
…path

The suite built PostAssistantFlowConfig with a nil Provider (a CON-86
regression) so the flow's cfg.Provider.Ref panicked at runtime. Construct a
real Provider and exercise the hybrid planner path by default, togglable to the
legacy single-Sonnet path via POST_ASSISTANT_PLANNER=false so one suite covers
both. Also assert content_delta streams on an edit — proving the editPost
writer sub-call reaches the client in the hybrid path.
In the hybrid path, action (from the planner) and content (from the editPost
writer) are decoupled, so a planner that claims action="edited" without calling
editPost would drive post.Content to "" and wipe the post. Guard it: with no
editResult and empty content, downgrade the turn to noted/declined and drop the
version snapshot. The legacy path is unaffected — there content is emitted
before action in the same JSON envelope, so the split can't occur.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 12, 2026

Copy link
Copy Markdown

CON-128

@grsmv
grsmv deployed to testing August 12, 2026 11:11 — with GitHub Actions Active
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The Post Assistant adds configurable planner and writer roles. Planner responses route edits through editPost; the writer generates Markdown and streams content deltas. Legacy generation remains available, while clone adaptation and planner safety handling use the writer path.

Changes

Hybrid Post Assistant

Layer / File(s) Summary
Configuration and template selection
src/config/config.go, src/genkit/flows/post_assistant/types.go, src/server/post_assistant.go, src/genkit/flows/post_assistant/flow.go
Planner mode and its 8192-token default are configurable. The flow loads planner and writer templates and passes the selected templates to execution.
Planner and writer prompts
src/genkit/flows/post_assistant/prompts/post_assistant.tmpl
The planner routes edits, answers, clones, restores, schedules, and notes. The writer generates complete Markdown content without explanations or JSON.
Writer tools and editing
src/genkit/flows/post_assistant/tools.go, src/genkit/flows/post_assistant/edit_tool_test.go
editPost validates instructions, invokes the writer, records results, and returns a receipt. Cross-platform clones use writer-generated adaptations when no content override exists. Asset excerpts are captured, deduplicated, and included in writer instructions. Tests cover invalid instructions, writer failures, excerpt inclusion, and deduplication.
Planner execution and validation
src/genkit/flows/post_assistant/run.go, src/genkit/flows/post_assistant/prewarm.go, src/integration/post_assistant_test.go
Planner and legacy paths select different models, tools, token limits, and usage metadata. Writer output becomes authoritative for edits, and missing writer results prevent empty content persistence. Prewarming selects the active model role, and SSE integration tests cover content_delta events.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PostAssistantFlow
  participant PlannerModel
  participant editPost
  participant WriterModel
  Client->>PostAssistantFlow: submit post instruction
  PostAssistantFlow->>PlannerModel: run planner with editPost
  PlannerModel->>editPost: send edit instruction
  editPost->>WriterModel: generate updated Markdown
  WriterModel-->>Client: stream content_delta events
  editPost-->>PostAssistantFlow: return edit receipt and content
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% 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 clearly and concisely summarizes the main change: splitting Post Assistant planning and writing across Haiku and Sonnet models.
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 💡 1
📝 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-128-post-assistant-hybrid-models

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.

@grsmv
grsmv deployed to testing August 12, 2026 11:15 — with GitHub Actions Active

@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: 5

🤖 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/post_assistant/prompts/post_assistant.tmpl`:
- Around line 91-97: Update the editPost-to-runWriter flow described in the post
assistant prompt so retrieved asset-tool content is passed as separate source
material alongside the unchanged, verbatim user instruction. Ensure asset-based
edits use the retrieved excerpts rather than only the asset preview, and add
coverage validating that this content reaches the writer.
- Around line 157-164: Update the editPost writer flow so retrieved asset chunks
from the planner are included in the writer’s input or context alongside the
original instruction. Ensure the writer can use this asset content when
producing asset-based edits, while preserving the existing response schema and
behavior for requests without retrieved chunks.

In `@src/genkit/flows/post_assistant/run.go`:
- Around line 494-505: Update the hybrid edit guard around result.Action and
st.editResult so every planner result claiming "edited" requires st.editResult
to be non-nil, regardless of result.UpdatedContent. When editPost was not
invoked, clear result.UpdatedContent before downgrading the action and retain
the existing noted/declined, SaveVersion, and explanation behavior.

In `@src/genkit/flows/post_assistant/tools.go`:
- Around line 438-442: Validate the adapted output returned by runWriter before
assigning it to content or creating the cross-platform clone. In the adaptation
flow around runWriter, treat an empty adapted string as an error and stop
processing, while preserving the existing wrapped error handling for non-nil
errors.
- Around line 426-433: Update the registered clonePost tool contract and planner
instructions to require omitting content for cross-platform clones so the
server-side writer handles adaptation. Document content as an explicit override
only, preserving the existing content check in the clone flow and ensuring
omitted content reaches runWriter and RoleGeneration.
🪄 Autofix

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: 2d0befda-94a2-4e53-b6cf-d835a59cd07d

📥 Commits

Reviewing files that changed from the base of the PR and between 42a610a and 1483e5d.

📒 Files selected for processing (10)
  • src/config/config.go
  • src/genkit/flows/post_assistant/edit_tool_test.go
  • src/genkit/flows/post_assistant/flow.go
  • src/genkit/flows/post_assistant/prewarm.go
  • src/genkit/flows/post_assistant/prompts/post_assistant.tmpl
  • src/genkit/flows/post_assistant/run.go
  • src/genkit/flows/post_assistant/tools.go
  • src/genkit/flows/post_assistant/types.go
  • src/integration/post_assistant_test.go
  • src/server/post_assistant.go

Comment thread src/genkit/flows/post_assistant/prompts/post_assistant.tmpl
Comment thread src/genkit/flows/post_assistant/prompts/post_assistant.tmpl
Comment thread src/genkit/flows/post_assistant/run.go Outdated
Comment thread src/genkit/flows/post_assistant/tools.go
Comment thread src/genkit/flows/post_assistant/tools.go
@grsmv
grsmv deployed to testing August 12, 2026 11:17 — with GitHub Actions Active
grsmv and others added 5 commits August 12, 2026 14:56
In the hybrid path the planner retrieves asset chunks, but the Sonnet writer
only saw the short previews in the context block — so asset-grounded edits
worked from the preview rather than the retrieved text.

Capture the chunks getAssetChunks/searchAssetChunks return on requestState
(deduped by chunk ID) and pass them to the writer as source material alongside
the unchanged, verbatim instruction (composeWriterInstruction). Update the
planner prompt to retrieve before editPost and note that retrieved excerpts are
handed to the writer. Adds coverage that the excerpts reach the writer input.
The editPost writer's system prompt described only "a single instruction" and
never mentioned the ## Source material section composeWriterInstruction now
appends. Note it in the writer block and add a rule to treat retrieved excerpts
as authoritative for asset-based facts, preferring them over the shorter asset
previews. Prompt-only; no behavior change for turns without retrieved chunks.
The hybrid safety guard only fired when result.UpdatedContent was also empty,
but in the planner path UpdatedContent is populated from scanner.Values() even
when only explanation is streamed. A planner that emitted inline content and
claimed "edited" without calling editPost would slip past the guard and persist
Haiku-written copy, defeating the split.

Drop the UpdatedContent condition so any "edited" result requires st.editResult
to be non-nil, and clear result.UpdatedContent before downgrading so the
illegitimate inline content is never persisted or returned. Keeps the existing
noted/declined, SaveVersion, and explanation behavior.
The registered clonePost tool description still told the model to "provide
content adapted to that platform" for a cross-platform clone, contradicting the
hybrid design (and the planner prompt): in the planner path the model must OMIT
content so the server's writer adapts it. A model following the stale contract
would pass content, bypassing the runWriter adaptation branch and leaking Haiku
copy.

Update the description to require omitting content for cross-platform clones
(server adapts, optional instruction to steer) and document content as an
explicit override only. Prompt/field docs and the content check were already
aligned; the legacy system block is left as-is (no server writer there).
runWriter can return ("", nil) when the model yields no text. In the clone
adaptation path that empty string was assigned to content, and the downstream
`if content != ""` guard then skipped the override — so a cross-platform clone
would silently produce a verbatim, unadapted copy of the source on the target
platform. Treat an empty adapted string as an error and stop, mirroring the
guard toolEditPost already has; the existing wrapped error handling for non-nil
errors is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@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.

🧹 Nitpick comments (1)
src/genkit/flows/post_assistant/edit_tool_test.go (1)

64-68: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert the complete excerpt and source identity labels.

The current checks search for only a substring and the generic Source material heading. They pass if the writer prompt truncates the excerpt or drops asset1 and c1. Assert the complete content and the emitted asset/chunk label.

Proposed test update
-	if !strings.Contains(out, "multiplexed onto OS threads") {
-		t.Fatalf("the retrieved excerpt content must reach the writer as source material; got: %q", out)
+	if !strings.Contains(out, excerpts[0].Content) {
+		t.Fatalf("the complete retrieved excerpt must reach the writer as source material; got: %q", out)
 	}
+	if !strings.Contains(out, "Asset asset1, chunk c1:") {
+		t.Fatalf("the source identity labels must reach the writer; got: %q", out)
+	}
🤖 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/post_assistant/edit_tool_test.go` around lines 64 - 68,
Strengthen the assertions in the relevant test around the writer output variable
out: require the complete retrieved excerpt text rather than a partial
substring, and verify the emitted source identity includes both asset1 and c1
instead of only the generic “Source material” heading. Preserve the existing
failure diagnostics while ensuring truncation or missing labels causes the test
to fail.
🤖 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.

Nitpick comments:
In `@src/genkit/flows/post_assistant/edit_tool_test.go`:
- Around line 64-68: Strengthen the assertions in the relevant test around the
writer output variable out: require the complete retrieved excerpt text rather
than a partial substring, and verify the emitted source identity includes both
asset1 and c1 instead of only the generic “Source material” heading. Preserve
the existing failure diagnostics while ensuring truncation or missing labels
causes the test to fail.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 98ca66cc-18cf-4d76-93b9-ccfc2a57be47

📥 Commits

Reviewing files that changed from the base of the PR and between 1483e5d and 63af902.

📒 Files selected for processing (4)
  • src/genkit/flows/post_assistant/edit_tool_test.go
  • src/genkit/flows/post_assistant/prompts/post_assistant.tmpl
  • src/genkit/flows/post_assistant/run.go
  • src/genkit/flows/post_assistant/tools.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/genkit/flows/post_assistant/prompts/post_assistant.tmpl
  • src/genkit/flows/post_assistant/run.go
  • src/genkit/flows/post_assistant/tools.go

@grsmv
grsmv deployed to testing August 12, 2026 14:08 — with GitHub Actions Active
@grsmv grsmv added the to test label Aug 12, 2026
@grsmv
grsmv merged commit 6439cad into main Aug 12, 2026
6 checks passed
@grsmv
grsmv deleted the feature/con-128-post-assistant-hybrid-models branch August 15, 2026 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant