Skip to content

fix(agent): use actual value in fillForm instead of hallucinated placeholder#1863

Open
guoyangzhen wants to merge 1 commit intobrowserbase:mainfrom
guoyangzhen:fix/fillform-value-parameter
Open

fix(agent): use actual value in fillForm instead of hallucinated placeholder#1863
guoyangzhen wants to merge 1 commit intobrowserbase:mainfrom
guoyangzhen:fix/fillform-value-parameter

Conversation

@guoyangzhen
Copy link

@guoyangzhen guoyangzhen commented Mar 20, 2026

Problem

The fillForm agent tool receives a value parameter but never uses it. When the tool calls observe(), it only passes the action description, causing the LLM to hallucinate placeholder values like test@example.com instead of using the actual provided value.

This affects:

  • Custom tool results (e.g., TOTP codes for 2FA flows)
  • Any scenario where the agent separates the value from the action string
  • Flaky behavior that depends on whether the LLM embeds the value in the action text

Fixes #1789

Root Cause

The fillForm input schema only had an action field. The observe() call used only the action description to identify elements, but the observe LLM then hallucinates what value to type instead of using the intended one.

Changes

  1. Added value field to the input schema — Each field now requires both an action (describing which element to target) and a value (the text to type)
  2. Override observe results with actual values — After observe() returns element identification results, the code injects the actual value into the observe result's arguments before calling act()
  3. Variable substitution support — Uses substituteVariables() to support variable tokens in values, matching the behavior of the type tool
  4. Updated descriptions — Action description now clarifies it describes the target element, not the value

Verification

The fix can be verified by:

  1. Using the agent to fill a form where the LLM generates action strings that don't embed the value, and confirming the actual value from the value field is used
  2. Testing with 2FA/TOTP flows where custom tools return exact values

Summary by cubic

Make the agent’s fillForm tool use the actual provided value for each field instead of hallucinated placeholders. This fixes flaky fills and ensures exact values (e.g., TOTP codes) are typed.

  • Bug Fixes

    • Add value to each field in the input schema; action now only identifies the target element.
    • Inject the provided value into observe() results before act(), with substituteVariables() support.
    • Clarify action/value descriptions.
  • Migration

    • Update all fillForm calls to include a value for each field.
    • Move any text previously embedded in action into value; keep action focused on the target (e.g., "type into the email input").
    • %variableName% tokens are supported in value when variables are available.

Written for commit 331efb3. Summary will update on new commits. Review in cubic

…eholder

The fillForm tool previously only passed an 'action' string to observe(),
which caused the LLM to hallucinate placeholder values like 'test@example.com'
instead of using the actual intended text.

Changes:
- Add explicit 'value' field to the fillForm input schema
- Override observe() results with the actual value before calling act()
- Use substituteVariables() to support %variableName% substitution
- Update action description to clarify action vs value separation

Fixes browserbase#1789
@changeset-bot
Copy link

changeset-bot bot commented Mar 20, 2026

⚠️ No Changeset found

Latest commit: 331efb3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@github-actions
Copy link
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 Mar 20, 2026
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 20, 2026

Greptile Summary

This PR fixes a real bug in the fillForm agent tool where the value parameter was never used, causing the observe LLM to hallucinate placeholder values (e.g. test@example.com) instead of typing the intended text. The fix adds a required value field to the input schema and injects the actual values into observe results before calling act(), following the same substituteVariables() pattern used by the type tool.

Key changes:

  • fillform.ts: Added value: z.string() to each field's schema alongside action; updated tool and field descriptions; overrides observeResult.arguments[0] with substituteVariables(fields[i].value, variables) after observe() returns.

Issues found:

  • The value-injection loop (observeResults[i]fields[i]) assumes the observe LLM returns results in the same order as the input fields. Because observe() uses a single combined instruction and the LLM is free to return elements in DOM or confidence order, a mismatch can silently swap values across fields—replacing the hallucination bug with a transposition bug for multi-field forms.
  • The actionDescription for the variables case still includes %variableName% guidance and an example ("type %email% into the email input"), but no substituteVariables call is applied to the action string before it is passed to observe(), so those tokens would be sent verbatim to the LLM.

Confidence Score: 3/5

  • Safe for the common single-field case, but the positional mapping assumption between observe results and input fields introduces a silent transposition risk for multi-field forms.
  • The hallucination fix is correct in intent and the substituteVariables integration is consistent with the rest of the codebase. However, the index-alignment assumption between observeResults and fields is not guaranteed by the observe LLM, which could cause wrong values to be typed into wrong fields without any error being raised. This is a potential regression for multi-field forms (the stated primary use-case of fillForm).
  • packages/core/lib/v3/agent/tools/fillform.ts — specifically the observeResults ↔ fields index mapping loop (lines 61–67)

Important Files Changed

Filename Overview
packages/core/lib/v3/agent/tools/fillform.ts Adds a value field to the fillForm input schema and injects actual values into observe results before calling act(). The value injection loop assumes positional correspondence between observeResults and fields, which the observe LLM does not guarantee, creating a potential value/field swap regression.

Sequence Diagram

sequenceDiagram
    participant LLM as Agent LLM
    participant FT as fillFormTool
    participant Obs as observe()
    participant Act as act()

    LLM->>FT: fields=[{action, value}, ...]
    FT->>Obs: instruction = join(f.action for f in fields)
    Obs-->>FT: observeResults[] (LLM-ordered)
    Note over FT: Loop: observeResults[i].arguments[0]<br/>= substituteVariables(fields[i].value)<br/>⚠️ assumes same ordering as input fields
    loop for each observeResult
        FT->>Act: act(res, {variables, timeout})
        Act-->>FT: actResult
    end
    FT-->>LLM: {success, actions, playwrightArguments}
Loading

Last reviewed commit: "fix(agent): use actu..."

Comment on lines +61 to +67
for (let i = 0; i < observeResults.length && i < fields.length; i++) {
const res = observeResults[i];
if (res.method === "fill" && res.arguments && res.arguments.length > 0) {
const actualValue = substituteVariables(fields[i].value, variables);
res.arguments[0] = actualValue;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Value/field index misalignment risk

The loop assumes observeResults[i] corresponds to fields[i] in a 1-to-1, order-preserving way. However, observe() delegates to an LLM which processes the combined instruction string and returns actions in whatever order it infers (typically DOM/accessibility-tree order, not declaration order). If the LLM returns results in a different sequence—or returns fewer results because one element wasn't found—the wrong value will be injected into the wrong field.

A concrete example: given fields = [{action: "email input", value: "user@example.com"}, {action: "password input", value: "s3cr3t"}], if the password field appears before the email field in the DOM, the observe LLM may return [{...password element...}, {...email element...}]. The loop would then do:

  • observeResults[0] (password element) ← fields[0].value = "user@example.com"
  • observeResults[1] (email element) ← fields[1].value = "s3cr3t"

In the old code the value was embedded in the action string so each observe result already carried its own value regardless of ordering. The new separation of value from action makes the ordering dependency a correctness issue.

A safer approach would be to make one observe() call per field (matching each result directly to its value), or to enrich the instruction with a unique tag per field and parse that tag back from the observe results to reconstruct the mapping.

Comment on lines 19 to +21
const actionDescription = hasVariables
? `Must follow the pattern: "type <exact value> into the <field name> <fieldType>". Use %variableName% to substitute a variable value. Available: ${Object.keys(variables).join(", ")}. Examples: "type %email% into the email input", "type %password% into the password input"`
: 'Must follow the pattern: "type <exact value> into the <field name> <fieldType>". Examples: "type john@example.com into the email input", "type John into the first name input"';
? `Describe which field to target, e.g. "type into the email input", "type into the password field". Use %variableName% to substitute a variable value. Available: ${Object.keys(variables).join(", ")}. Example: "type %email% into the email input"`
: 'Describe which field to target, e.g. "type into the email input", "type into the first name input"';
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Variable token in action description is misleading

The actionDescription for the hasVariables case still mentions %variableName% syntax and includes the example "type %email% into the email input". This suggests putting variable tokens in the action field, but the action string is used only to build the observe() instruction and no substituteVariables() call is ever applied to it—so any %variableName% token placed there would be passed verbatim to the observe LLM and never resolved.

Since the intent of this PR is to separate element targeting (action) from value typing (value), the action description should be cleaned up to remove the variable-substitution guidance entirely:

Suggested change
const actionDescription = hasVariables
? `Must follow the pattern: "type <exact value> into the <field name> <fieldType>". Use %variableName% to substitute a variable value. Available: ${Object.keys(variables).join(", ")}. Examples: "type %email% into the email input", "type %password% into the password input"`
: 'Must follow the pattern: "type <exact value> into the <field name> <fieldType>". Examples: "type john@example.com into the email input", "type John into the first name input"';
? `Describe which field to target, e.g. "type into the email input", "type into the password field". Use %variableName% to substitute a variable value. Available: ${Object.keys(variables).join(", ")}. Example: "type %email% into the email input"`
: 'Describe which field to target, e.g. "type into the email input", "type into the first name input"';
? `Describe which field to target, e.g. "the email input", "the password field".`
: 'Describe which field to target, e.g. "the email input", "the first name input"';

Copy link
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.

No issues found across 1 file

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.
Architecture diagram
sequenceDiagram
    participant Agent as LLM Agent
    participant Tool as fillFormTool
    participant Vars as Variable Utils
    participant Engine as V3 Engine (Observe/Act)

    Note over Agent,Tool: NEW: Input schema now strictly separates<br/>Target (action) and Content (value)

    Agent->>Tool: execute(fields: [{action, value}])
    
    Tool->>Engine: v3.observe(instruction)
    Note right of Engine: LLM finds element but may<br/>hallucinate "value" argument
    Engine-->>Tool: observeResults (hallucinated values)

    loop For each field/result pair
        Tool->>Vars: NEW: substituteVariables(field.value, variables)
        Vars-->>Tool: actualValue
        
        Note over Tool: NEW: Inject actualValue into observeResults<br/>overriding LLM-generated arguments
        Tool->>Tool: res.arguments[0] = actualValue
        
        Tool->>Engine: v3.act(res)
        Engine-->>Tool: status
    end

    Tool-->>Agent: completion status
Loading

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

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.

fillForm agent tool drops value parameter, causing LLM to hallucinate fill values

2 participants