Skip to content

feat(sdk): adopt PostHog's intent description as the default (0.3.1) - #70

Merged
marselsel merged 2 commits into
mainfrom
feat/posthog-intent-description
Aug 7, 2026
Merged

feat(sdk): adopt PostHog's intent description as the default (0.3.1)#70
marselsel merged 2 commits into
mainfrom
feat/posthog-intent-description

Conversation

@marselsel

Copy link
Copy Markdown
Collaborator

Makes PostHog's wording the SDK default, so both instrumented apps pick it up from an SDK bump rather than each hardcoding a string.

Why

The old default prescribed a word count, third person and a worked example — so the model copied the example's shape. Of 59 intents captured on Preisvergleich Prod in the 30 days to 2026-08-07, the ChatGPT ones averaged 122 characters and nearly every one opened the same way:

"Comparing German private current accounts for a €2,500 monthly income to identify suitable fees, conditions, and account options."
"Comparing one-year fixed-deposit offers to help the user evaluate returns on a €10,000 investment."
"Comparing German instant-access savings accounts to help the user find competitive returns for a €10,000 deposit."

Informative, and interchangeable. The new default is one open question — and costs ~11 words of every tool's schema instead of ~40.

- "State in 15-25 words, third person, why this tool is being called and how it
-  serves the user's current goal. Never include credentials or personal data.
-  Example: \"Searching order history for recent shipments…\""
+ "Why are you calling this tool? Briefly describe the user's goal."

The privacy clause is gone on purpose

The old text asked the model never to include personal data. Look at the samples above — it included stated monthly income and investment amounts anyway. A description is an instruction the model can reinterpret; it was never a control. The controls that do hold are unconditional and downstream: stripPii() over intent_signals in buildToolCallEvent, and the 500-character clamp. The docs now say that outright, with a callout, instead of leaving readers to assume the description protects them.

If you'd rather keep a privacy sentence anyway, say so and I'll put one back — but I'd rather it not read as a guarantee.

Two bugs found while bumping the version

Both fixed here, and worth knowing about independently of the wording:

  1. SDK_VERSION is hardcoded in two places — the server entrypoint and the React bundle, which can't import the server one — but only the server copy was checked against package.json. A release could have shipped widget events stamped 0.3.0 while everything else said 0.3.1, with nothing to catch it. Both are covered now. Verified with teeth: leaving the React constant behind fails with expected '0.3.0' to be '0.3.1'.
  2. Three React tests asserted the literal "0.3.0" rather than the constant, so they broke on every version bump for no reason. They compare against SDK_VERSION now, with the value itself guarded by the package.json checks above.

Verified

300/300 SDK tests pass, typecheck and lint clean. Confirmed against a real McpServer that the advertised tools/list schema carries context.description = "Why are you calling this tool? Briefly describe the user's goal.", and that no context is advertised when intent is off.

What follows this

This is the first of three steps and the only one that needs your review:

  1. this PR → merge
  2. release @yavio/sdk 0.3.1 — tag sdk-v0.3.1, OIDC publish; manual and yours
  3. bump both apps to 0.3.1 and deploy — Shipal (no override needed, it just inherits the default) and Preisvergleich (where I'd also drop the hardcoded override from marselsel/headlessbroker-mcp#10, which this supersedes)

One expectation to set: Shipal will take the new wording but won't produce more intents until its capture bug is fixed — it's at 3.2% coverage versus Preisvergleich's 100%. The signal for whether this wording is better will come from Preisvergleich.

Our default told the model to use 15-25 words, third person, AND gave it a
worked example — so the model copied the example's shape. Of 59 intents
captured on Preisvergleich Prod in the 30 days to 2026-08-07, the ChatGPT ones
averaged 122 characters and nearly every one opened "Comparing X to help the
user Y":

  "Comparing German private current accounts for a €2,500 monthly income to
   identify suitable fees, conditions, and account options."
  "Comparing one-year fixed-deposit offers to help the user evaluate returns
   on a €10,000 investment."

Informative, and interchangeable — which defeats the point of a free-text
field. PostHog ships one unconstrained question instead, so that is now the
default verbatim: "Why are you calling this tool? Briefly describe the user's
goal." It also costs ~11 words of every tool's schema instead of ~40.

Deliberately dropped: the old "Never include credentials or personal data"
clause. It was not doing the job — the samples above carry income and
investment figures with that sentence in place — and a description the model
may reinterpret is the wrong layer for a privacy guarantee. The guarantees that
actually hold are unconditional and downstream: stripPii() over intent_signals
in buildToolCallEvent, and the 500-character clamp. The docs now say so
explicitly rather than implying the description protects anything.

Two things found while bumping the version, both fixed here:

- SDK_VERSION is hardcoded in TWO places (server entrypoint and the React
  bundle, which cannot import the server one) but only the server copy was
  checked against package.json. A release could have shipped widget events
  stamped 0.3.0 while everything else said 0.3.1, silently. Both are now
  covered; the new test fails with "expected '0.3.0' to be '0.3.1'" when the
  React constant is left behind.
- Three React tests asserted the literal "0.3.0" rather than the constant, so
  they broke on every version bump for no reason. They now compare against
  SDK_VERSION, and the value itself is guarded by the package.json checks.

300/300 SDK tests pass.
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Coverage Report for sdk

Status Category Percentage Covered / Total
🔵 Lines 92.14% 1878 / 2038
🔵 Statements 92.14% 1878 / 2038
🔵 Functions 95.65% 132 / 138
🔵 Branches 82.13% 639 / 778
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/sdk/src/core/config.ts 100% 96.42% 100% 100%
packages/sdk/src/core/version.ts 100% 100% 100% 100%
packages/sdk/src/react/constants.ts 100% 100% 100% 100%
packages/sdk/src/server/index.ts 74.35% 75% 50% 74.35% 34-37, 40-43, 54-55
packages/sdk/src/server/intent.ts 85.23% 65.38% 100% 85.23% 90, 95-96, 120-121, 128-129, 140-143, 154, 157-160, 213-214, 296-297, 303, 330-331, 335-336, 354-355, 368, 379-380, 392
Generated in workflow #200 for commit 699698b by the Vitest Coverage Report Action

…lamping

Follow-up to the review of this PR. The wording change was fine; the
justification around it was not, and one real ordering bug came with it.

The docs callout I added promised "intent_signals is PII-stripped" as though
that settled the matter — while citing, two sentences earlier, an income figure
the stripper provably cannot catch. stripPii matches exactly four shapes
(email, Luhn-valid card, US SSN, US-format phone) and the ingest layer adds
ITIN and US street addresses; pii.ts calls itself "best-effort" and
"defense-in-depth" for good reason. A customer citing that sentence in a DPIA
would have been relying on something the code does not do. The docs now name
the four shapes, list what is stored verbatim (income, names, employers,
non-US formats, pasted credentials), and say plainly that `intent: false` is
the answer for regulated data.

The config comment had the same problem in reverse: it justified deleting the
"never include personal data" clause by asserting the downstream layer was the
real guarantee. It is not, and the comment's own evidence proves it. It now
says the trade was a weak control for none, and no longer forbids re-adding a
sentence — it just refuses to call one a control.

Ordering bug, found by the same review and fixed here: normalizeIntent clamped
to 500 and left redaction to buildToolCallEvent, so

- PII straddling the 500-char boundary was truncated into a fragment the
  patterns no longer match — "alice@corp." with the TLD sliced off shipped
  looking scrubbed while still being linkable; and
- redaction tokens are longer than what they replace ("a@b.co" 6 chars ->
  "[EMAIL_REDACTED]" 16), so the stored value could exceed the 500 characters
  the docs promise.

Redaction now runs before the clamp, behind a generous safety ceiling that
bounds the regex work on unbounded model output. The old clamp test used
"x".repeat(), which redaction never touches, so it could not have caught
either half.

Also from the review:

- SDK_VERSION is now a single core/version.ts re-exported by both entrypoints,
  rather than two literals plus a test to police them. The React bundle could
  always import core/ (react/hook.ts already imports core/pii.js), so the
  duplication was never structural. The test now guards the arrangement — it
  fails if a literal reappears — instead of the value.
- intent_signals had no redaction test at all; deleting the stripPii call left
  the suite green. It now has one.
- The docs quote the default description verbatim, which reintroduced the
  "edit both" drift this PR removed elsewhere. A test pins the page to
  DEFAULT_INTENT_DESCRIPTION.
- The docs example was itself a third-person gerund sentence — the exact
  converged shape the surrounding paragraph criticises. Replaced, and labelled
  as not a template.

All four new guards verified to fail when the behaviour they cover is broken.
305/305 SDK tests pass.
@marselsel
marselsel merged commit 4099786 into main Aug 7, 2026
12 checks passed
@marselsel
marselsel deleted the feat/posthog-intent-description branch August 7, 2026 12:40
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