Skip to content

fix: unwrap Anthropic $PARAMETER_NAME wrapper in tool responses (fixes #1986)#2026

Open
kagura-agent wants to merge 6 commits into
browserbase:mainfrom
kagura-agent:fix/unwrap-anthropic-parameter-name
Open

fix: unwrap Anthropic $PARAMETER_NAME wrapper in tool responses (fixes #1986)#2026
kagura-agent wants to merge 6 commits into
browserbase:mainfrom
kagura-agent:fix/unwrap-anthropic-parameter-name

Conversation

@kagura-agent
Copy link
Copy Markdown

@kagura-agent kagura-agent commented Apr 22, 2026

Problem

When using Anthropic models (e.g., anthropic/claude-sonnet-4-5) with act() or extract(), Claude wraps tool_use output in a { $PARAMETER_NAME: { ... } } envelope. This causes Zod schema validation to fail with AI_NoObjectGeneratedError because all expected fields appear undefined at the top level.

Reported in #1986.

Fix

Add a small unwrapToolResponse() helper that detects single-key objects with a $-prefixed key and strips the wrapper:

  • AnthropicClient.ts — unwrap toolUse.input before returning to callers
  • aisdk.ts — catch NoObjectGeneratedError, attempt to JSON-parse err.text, unwrap and re-validate against the schema. If recovery succeeds, return normally; if not, throw the original error

The helper is defensive: it only unwraps when the object has exactly one $-prefixed key, so non-Anthropic responses pass through unchanged.

Changes

File Change
unwrapToolResponse.ts New utility — detect and strip $PARAMETER_NAME wrapper
AnthropicClient.ts Apply unwrapToolResponse() to toolUse.input
aisdk.ts Recovery path in NoObjectGeneratedError catch block
unwrapToolResponse.test.ts 8 unit tests covering unwrap, passthrough, edge cases

Testing

  • 8 unit tests for the helper (wrapped objects, non-wrapped, arrays, null, primitives, empty objects, multi-key objects)
  • No changes to existing tests required (additive fix)

Summary by cubic

Fixes schema validation failures for Anthropic tool responses by unwrapping the $PARAMETER_NAME envelope so act()/extract() return a flat object for models like anthropic/claude-sonnet-4-5. Defers error logging to avoid duplicate LLM response logs when recovery succeeds (fixes #1986).

  • Bug Fixes
    • Inlined unwrapToolResponse() in AnthropicClient.ts to strip single $-prefixed wrappers; no-op otherwise.
    • Applied unwrap to toolUse.input; in aisdk added recovery to JSON-parse error text, unwrap, re-validate, and return; deferred error logging so only the success log emits on recovery.
    • Added unit tests and updated imports.

Written for commit 9ad7d1c. Summary will update on new commits. Review in cubic

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 22, 2026

🦋 Changeset detected

Latest commit: 9ad7d1c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@browserbasehq/stagehand Patch
@browserbasehq/stagehand-evals Patch
@browserbasehq/stagehand-server-v3 Patch
@browserbasehq/stagehand-server-v4 Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

This PR is from an external contributor and must be approved by a stagehand team member with write access before CI can run.
Approving the latest commit mirrors it into an internal PR owned by the approver.
If new commits are pushed later, the internal PR stays open but is marked stale until someone approves the latest external commit and refreshes it.

@github-actions github-actions Bot added external-contributor Tracks PRs mirrored from external contributor forks. external-contributor:awaiting-approval Waiting for a stagehand team member to approve the latest external commit. labels Apr 22, 2026
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 4 files

Confidence score: 3/5

  • A recoverable wrapper-unwrapping path in packages/core/lib/v3/llm/aisdk.ts can emit both error and success LLM response events for the same requestId, which may confuse flow records and downstream consumers.
  • Given the medium severity and clear behavioral change in event logging, there is some user-impacting risk despite being limited in scope.
  • Pay close attention to packages/core/lib/v3/llm/aisdk.ts - duplicate error/success logging for a single requestId.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/core/lib/v3/llm/aisdk.ts">

<violation number="1" location="packages/core/lib/v3/llm/aisdk.ts:310">
P2: Recoverable wrapper-unwrapping now logs two LLM response events (error then success) for the same requestId, creating conflicting flow records.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant App as Stagehand / App
    participant AISDK as AISDK Wrapper (aisdk.ts)
    participant Anthropic as AnthropicClient
    participant Tool as unwrapToolResponse Utility
    participant Schema as Zod Schema
    participant API as Anthropic API
    participant Logger as FlowLogger

    Note over App, API: Primary Flow: Proactive Unwrapping in Anthropic Client

    App->>Anthropic: createChatCompletion(options)
    Anthropic->>API: Send Request (tool_use)
    API-->>Anthropic: Response with {$PARAM: {...}}
    
    Anthropic->>Anthropic: Find tool_use content
    Anthropic->>Tool: NEW: unwrapToolResponse(toolUse.input)
    Note right of Tool: Strips single keys starting with "$"
    Tool-->>Anthropic: Unwrapped data object
    Anthropic-->>App: Return cleaned response

    Note over App, API: Secondary Flow: Recovery Path in AISDK Wrapper

    App->>AISDK: act() / extract()
    AISDK->>AISDK: generateObject()
    
    alt CHANGED: AI_NoObjectGeneratedError caught
        AISDK->>Tool: NEW: unwrapToolResponse(parsedErrorText)
        
        alt Successful Unwrap
            AISDK->>Schema: NEW: Re-validate unwrapped data
            
            alt Schema Valid
                AISDK->>Logger: NEW: logLlmResponse (Recovery)
                AISDK-->>App: Return Validated Data
            else Schema Still Invalid
                AISDK-->>App: Throw original NoObjectGeneratedError
            end
        else No "$" wrapper found
            AISDK-->>App: Throw original NoObjectGeneratedError
        end
    else Success path
        AISDK-->>App: Return Data
    end
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Comment thread packages/core/lib/v3/llm/aisdk.ts
Comment thread packages/core/lib/v3/llm/unwrapToolResponse.ts Outdated
@github-actions github-actions Bot added external-contributor:mirrored An internal mirrored PR currently exists for this external contributor PR. and removed external-contributor:awaiting-approval Waiting for a stagehand team member to approve the latest external commit. labels Apr 23, 2026
@github-actions github-actions Bot closed this Apr 23, 2026
@pirate pirate reopened this Apr 23, 2026
@browserbase browserbase deleted a comment from github-actions Bot Apr 23, 2026
@github-actions github-actions Bot added external-contributor:awaiting-approval Waiting for a stagehand team member to approve the latest external commit. and removed external-contributor:mirrored An internal mirrored PR currently exists for this external contributor PR. labels Apr 23, 2026
@pirate
Copy link
Copy Markdown
Member

pirate commented Apr 23, 2026

couple fixes needed but otherwise great fix, thanks for working on this!

@browserbase browserbase deleted a comment from github-actions Bot Apr 23, 2026
… on recovery

When wrapper-unwrapping recovery succeeds, only the success log is now
emitted. The error log is deferred to after the recovery attempt, so it
only fires when the error is actually thrown.
Per reviewer feedback, inline the utility into AnthropicClient.ts
instead of keeping it as a standalone file. Update imports in
aisdk.ts and the unit test accordingly.
@kagura-agent
Copy link
Copy Markdown
Author

Done! Moved unwrapToolResponse into AnthropicClient.ts (since it's an Anthropic-specific workaround) and updated the import in aisdk.ts + the unit test. The standalone file is removed. All 8 unit tests pass. Thanks for the review! 🌸

@github-actions
Copy link
Copy Markdown
Contributor

The latest approval by @pirate could not refresh the mirrored PR automatically (missing-previous-source). The external PR stays open, and the mirrored PR should be updated manually before work continues.

@github-actions github-actions Bot added external-contributor:mirrored An internal mirrored PR currently exists for this external contributor PR. and removed external-contributor:awaiting-approval Waiting for a stagehand team member to approve the latest external commit. labels Apr 23, 2026
@github-actions github-actions Bot closed this Apr 23, 2026
@pirate pirate reopened this Apr 23, 2026
@github-actions github-actions Bot added external-contributor:awaiting-approval Waiting for a stagehand team member to approve the latest external commit. and removed external-contributor:mirrored An internal mirrored PR currently exists for this external contributor PR. labels Apr 23, 2026
@kagura-agent
Copy link
Copy Markdown
Author

The CI failures appear to be pre-existing infrastructure issues unrelated to this change:

  • preview (Build SDKs) — fails with Error: Input required and not supplied: project (missing Vercel project config for external PRs)
  • server/v3/integration/* — all fail because BROWSERBASE_API_KEY is empty (secrets not available to external contributor PRs)
  • evals/regression — same issue: BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID are empty

All code-related checks pass: Build ✅, Lint ✅, CLI Tests ✅, and all core unit tests ✅ (including the new core/unwrapToolResponse tests).

@kagura-agent
Copy link
Copy Markdown
Author

Second ping — this has been open for 11 days. Is there anything I should adjust? Happy to iterate on the approach or close if it's not needed. 🙏

@kagura-agent
Copy link
Copy Markdown
Author

Closing this as it's been 13 days with no maintainer response after two pings. Happy to reopen or recreate if this fix is still desired — just let me know. Thanks! 🙏

@pirate pirate reopened this May 5, 2026
@kagura-agent
Copy link
Copy Markdown
Author

Following through on my earlier comment — closing this PR as it's been inactive for too long. Happy to reopen if the fix is still desired.

@pirate
Copy link
Copy Markdown
Member

pirate commented May 5, 2026

I re-opened it because the issue is valid still, please dont re-close it.

@pirate pirate reopened this May 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contributor:awaiting-approval Waiting for a stagehand team member to approve the latest external commit. external-contributor Tracks PRs mirrored from external contributor forks.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

act() fails with Anthropic models: response wrapped in $PARAMETER_NAME key

2 participants