Skip to content

[BUG] SyntaxError instead of MaxTokensError when tool_use JSON is truncated #1061

@cmbullock

Description

@cmbullock

Checks

  • I have updated to the lastest minor and patch version of Strands
  • I have checked the documentation and this is not expected behavior
  • I have searched ./issues and there are no duplicates of my issue

Strands Version

1.1.0

Node.js Version

22.17.0

Operating System

macOS 15.1

Installation Method

npm

Steps to Reproduce

  1. define a tool with multiple input fields
  2. configure BedrockModel with sufficiently low maxTokens
  3. prompt agent to call the tool with sufficiently long input to exceed max tokens

Expected Behavior

Failures due to output token limits being exceeded should generate a MaxTokensError rather than failing to parse truncated JSON. The python SDK exhibits this behavior.

Actual Behavior

The contentBlockStop arrives with truncated input; parse fails before messageStop { stopReason: 'maxTokens' } is processed. This deviates from the behavior observed in the python SDK.

Additional Context

Minimal failing typescript example:

import { Agent, BedrockModel, MaxTokensError, ModelError, tool } from '@strands-agents/sdk'
import { z } from 'zod'

const writeSections = tool({
  name: 'write_sections',
  description: 'Submit the six sections of a short essay, one per field.',
  inputSchema: z.object({
    intro: z.string().describe('Opening section.'),
    body: z.string().describe('Body section.'),
    middle: z.string().describe('Middle section.'),
    conclusion: z.string().describe('Conclusion section.'),
    summary: z.string().describe('Summary section.'),
    epilogue: z.string().describe('Epilogue section.'),
  }),
  callback: () => 'sections received',
})

const PROMPT = [
  'Call write_sections once with all six fields populated. Use one Lorem Ipsum',
  'sentence per field. Do not include any preamble or commentary — go straight',
  'to the tool call. The sentences to use:',
  '',
  'intro: Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
  'body: Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
  'middle: Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.',
  'conclusion: Nisi ut aliquip ex ea commodo consequat duis aute irure dolor.',
  'summary: In reprehenderit in voluptate velit esse cillum dolore eu fugiat.',
  'epilogue: Excepteur sint occaecat cupidatat non proident sunt in culpa qui.',
].join('\n')

const MAX_TOKENS = 180

const model = new BedrockModel({
  modelId: 'global.anthropic.claude-sonnet-4-5-20250929-v1:0',
  maxTokens: MAX_TOKENS,
})

const agent = new Agent({
  model,
  tools: [writeSections],
})

// Fails with ModelError(SyntaxError)
await agent.invoke(PROMPT)

Passing python example

import os
import traceback

from strands import Agent, tool
from strands.models import BedrockModel
from strands.types.exceptions import MaxTokensReachedException


@tool
def write_sections(
    intro: str,
    body: str,
    middle: str,
    conclusion: str,
    summary: str,
    epilogue: str,
) -> str:
    """Submit the six sections of a short essay, one per field."""
    return "sections received"


PROMPT = "\n".join(
    [
        "Call write_sections once with all six fields populated. Use one Lorem Ipsum",
        "sentence per field. Do not include any preamble or commentary — go straight",
        "to the tool call. The sentences to use:",
        "",
        "intro: Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
        "body: Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
        "middle: Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",
        "conclusion: Nisi ut aliquip ex ea commodo consequat duis aute irure dolor.",
        "summary: In reprehenderit in voluptate velit esse cillum dolore eu fugiat.",
        "epilogue: Excepteur sint occaecat cupidatat non proident sunt in culpa qui.",
    ]
)

MAX_TOKENS = 180

model = BedrockModel(
    model_id="global.anthropic.claude-sonnet-4-5-20250929-v1:0",
    max_tokens=MAX_TOKENS,
)

agent = Agent(model=model, tools=[write_sections])

# Fails correctly with MaxTokensReachedException
agent(PROMPT)

Possible Solution

Regression introduce in #680

Restore the deferred-throw pattern but use stopReason to disambiguate root cause from symptom:

  } catch (e: unknown) {
    if (e instanceof SyntaxError) {
      deferredParseError = e
    }
  }

  // ... after the loop ...
  if (finalStopReason === 'maxTokens') {
    throw new MaxTokensError(..., stoppedMessage)   // root cause wins
  }
  if (deferredParseError !== undefined) {
    throw deferredParseError                         // model emitted bad JSON
  }

This should keep the scenario in #680 working (when stopReason is not maxTokens, the SyntaxError still surfaces) and fixes the truncation case. The Python SDK uses the same disambiguation strategy.

Related Issues

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions