Skip to content

fix: use appendSystemPrompt for JSON mode instructions for Claude Code#889

Closed
ben-vargas wants to merge 3 commits into
eyaltoledano:nextfrom
ben-vargas:fix/claude-code-append-sys-prompt
Closed

fix: use appendSystemPrompt for JSON mode instructions for Claude Code#889
ben-vargas wants to merge 3 commits into
eyaltoledano:nextfrom
ben-vargas:fix/claude-code-append-sys-prompt

Conversation

@ben-vargas

Copy link
Copy Markdown
Contributor

Fix: Use appendSystemPrompt for JSON mode instructions in Claude Code provider

Summary

This PR fixes an issue where JSON formatting instructions were being appended directly to the user's prompt in the Claude Code provider. The instructions are now properly sent through the appendSystemPrompt option, which should improve JSON parsing reliability and maintain cleaner separation between user content and system instructions.

Problem

When using the Claude Code provider in JSON mode (object-json), the system was appending JSON formatting instructions directly to the user's prompt. This approach had several issues:

  1. JSON instructions were visible in the conversation history
  2. Instructions were mixed with user content, potentially affecting the model's understanding
  3. Users reported parsing issues with Claude Code's JSON responses (possible fix for bug: Claude Code JSON Generation Fails with "Unterminated string in JSON" Error #887)
  4. The approach didn't align with Claude Code SDK's intended usage patterns

Solution

The fix moves JSON formatting instructions from the user prompt to the appendSystemPrompt parameter:

  1. Modified message-converter.js:

    • Removed JSON instructions from being appended to finalPrompt
    • Added a new return property jsonModeInstruction containing the formatting rules
    • Preserved all existing functionality for non-JSON modes
  2. Updated language-model.js:

    • Both doGenerate and doStream methods now extract jsonModeInstruction
    • Dynamically append JSON instructions to appendSystemPrompt when in object-json mode
    • Properly handle cases where appendSystemPrompt already exists (concatenate with newlines)

Changes

  • src/ai-providers/custom-sdk/claude-code/message-converter.js: Return JSON instructions separately instead of appending to prompt
  • src/ai-providers/custom-sdk/claude-code/language-model.js: Use appendSystemPrompt for JSON instructions in both generate and stream methods

Testing

  • All existing tests pass (601 tests passed, 11 skipped, 0 failed)
  • Format check passes (Biome)
  • No breaking changes to existing functionality
  • JSON mode behavior is preserved but with improved implementation

Impact

This change should improve:

  • JSON parsing reliability when using Claude Code provider
  • Cleaner separation of concerns between user content and system instructions
  • Better alignment with Claude Code SDK best practices

Notes

This is a non-breaking change that maintains backward compatibility while improving the implementation of JSON mode in the Claude Code provider.

… provider

- Move JSON formatting instructions from user prompt to appendSystemPrompt
- Return jsonModeInstruction from message converter instead of appending to prompt
- Update both doGenerate and doStream methods to handle JSON instructions properly
- This should improve JSON parsing reliability by using proper system prompts
@ben-vargas ben-vargas marked this pull request as draft June 27, 2025 05:49
@ben-vargas

Copy link
Copy Markdown
Contributor Author

Converting to draft until someone who is having parsing issues tests to confirm if (A) the parsing issues are improved (there may need to be some parsing cleanup too), and (B) testing to ensure it doesn’t make things worse.

@dhabedank

Copy link
Copy Markdown

Hi @ben-vargas - I'd love to test this as its a persistent issue for me.

@dhabedank

dhabedank commented Jun 28, 2025

Copy link
Copy Markdown

Thanks for your work on this @ben-vargas - here's what I found. To be honest, I am quite novice. I'm not completely certain I applied your new code correctly. If you have any recommendations on how to test - please let me know!

  • I applied the patch by downloading the PR patch file, navigating to my global task-master-ai install directory, and running patch -p1 < pr-889.patch to update the codebase.

  • I'm testing parsing using the MCP with Claude-Code as a provider, but results are still very shaky. As a workaround, I've switched to using the CLI directly.

  • My setup: Opus is the main and research model, with Sonnet as fallback.

  • It seems I can't reliably parse for more than 10 tasks. My PRD is about 1,600 lines; attempts with --num-tasks=30, 20, 19, and 15 all fail similarly.

  • Notably, parsing performance degrades after several retries. For example, the very first parse attempt in a blank folder (after re-initializing task-master-ai) works for 10 tasks, but increasing the task count leads to repeated failures.

  • Typical error:

    [ERROR] Claude Code object generation failed: Unterminated string in JSON at position 8000 (line 1 column 8001)
    
  • This occurs for all roles (main, fallback, research) and across both Opus and Sonnet models.

Let me know if you need more details about my process or the errors!

Edit - the error codes seem to change from parse-to-parse. Here is another one when trying again parsing for 30 tasks:

⠹ Parsing PRD and generating tasks...
[ERROR] Claude Code object generation failed: Unterminated string in JSON at position 16000 (line 1 column 16001) {"error":{"name":"AI_APICallError","url":"claude-code-cli://command","requestBodyValues":{"prompt":"You are an AI assistant specialized in analyzing Product Requirements Documents (PRDs) and generating a structured, logically ordered, dependency-aware and sequenced list of development tasks in JSON "},"isRetryable":false,"data":{"promptExcerpt":"You are an AI assistant specialized in analyzing Product Requirements Documents (PRDs) and generating a structured, logically ordered, dependency-aware and sequenced list of development tasks in JSON "}}}
[WARN] Attempt 1 failed for role main (generateObject / claude-code): Claude Code API error during object generation: Unterminated string in JSON at position 16000 (line 1 column 16001)
[ERROR] Something went wrong on the provider side. Max retries reached for role main (generateObject / claude-code).
[ERROR] Service call failed for role main (Provider: claude-code, Model: opus): Claude Code API error during object generation: Unterminated string in JSON at position 16000 (line 1 column 16001)

Ported critical JSON parsing improvements from ai-sdk-provider-claude-code
to enhance reliability and performance of JSON extraction in the custom SDK.

Key improvements:
- Replace basic regex patterns with jsonc-parser for robust parsing
- Implement O(n) bracket-matching algorithm vs previous O(n²) approach
- Add support for JSON with comments (JSONC) and trailing commas
- Better handling of markdown code fences and variable declarations
- Efficient fallback strategy for malformed JSON (limited to 1000 chars)
- Return formatted JSON via JSON.stringify for consistency

Performance gains:
- Large valid JSON: <100ms (previously could timeout)
- JSON with trailing garbage: <200ms with 10KB of noise
- Deeply nested malformed JSON: <500ms (prevents hanging)

Changes:
- Install jsonc-parser dependency
- Rewrite json-extractor.js with optimized implementation
- Add comprehensive test suite with 26 tests covering edge cases
- Verify compatibility with existing language-model.js usage

This addresses potential issues with Claude's varied response formats
and significantly improves performance for large or malformed JSON responses.
Previously, abort event listeners were added to AbortSignal without being
removed, potentially causing memory leaks when reusing the same signal
across multiple API calls. This was a critical issue that could lead to
resource exhaustion in production environments.

Changes implemented:
- Add proper cleanup of abort event listeners in finally blocks
- Use { once: true } option for automatic listener removal
- Add cancel handler for stream to ensure cleanup on cancellation
- Ensure listeners are removed even when errors occur

The fix applies to both doGenerate() and doStream() methods:
- Store abort listener reference to enable removal
- Add event listener with { once: true } for auto-cleanup
- Remove listener in finally block as defensive measure
- Add stream cancel handler to clean up on early termination

Added comprehensive test coverage:
- Verify listeners use { once: true } option
- Confirm cleanup in finally blocks
- Test cleanup on error conditions
- Validate stream cancellation cleanup
- Ensure no listener accumulation with signal reuse

This prevents memory leaks and follows best practices for event listener
management, matching the fix implemented in ai-sdk-provider-claude-code
(commit 4017a76).
@changeset-bot

changeset-bot Bot commented Jun 28, 2025

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 2069934

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

@ben-vargas

Copy link
Copy Markdown
Contributor Author

@dhabedank - Thanks for testing. I'm not sure if that patch method would have applied properly or not. I typically just use something like gh pr checkout 889 to checkout the pr branch (or ask claude code to get it). I also have to make sure the npm installation is using the right version and not the globally installed version (again, claude code handy to do all that for you).

I'm trying to break it now by using more than 10 tasks. I asked Claude Code to create a PRD (crm.prd) for a CRM and then I parsed this out with --num-tasks=30 successfully.

CleanShot 2025-06-28 at 11 59 32@2x

I wonder if you can give this a try? I had already committed those two commits above with additional updates from my ai-sdk-provider-claude-code repo that hadn't yet been implemented in the custom-sdk built into task-master.

So at the moment I don't know if any of these changes helped resolved the issue, or if I'm still just not testing it right to get it to break. I guess I can switch back to the main release and test the crm.prd again to see...

@ben-vargas

ben-vargas commented Jun 28, 2025

Copy link
Copy Markdown
Contributor Author

No, switching to the main npm release parsed successfully too. I'm still not confident I can even replicate the parsing issue(s) everyone is seeing. I guess I need an actual PRD from someone that is failing (or obfuscated or made-up one similar to what I'm doing), but that is confirmed failing along with the exact parse-prd flags being used.

@nwentling5

Copy link
Copy Markdown

@ben-vargas Thank you for your ongoing efforts to resolve this issue.

I am still running into the same issue with master and this PR.

Some background:

  • I am on the $200 Max Plan
  • I've updated $CLAUDE_CODE_MAX_OUTPUT_TOKENS in my .zsh file and confirmed with echo that it is set to 32000.
  • On the master branch, I was able to parse your tetris.prd without issue, but couldn't parse my own prd.

Here is the output from running task-master parse-prd .taskmaster/docs/crm.prd --force --num-tasks=30 on your crm.prd above:

CleanShot 2025-07-03 at 14 27 16@2x

@ben-vargas

Copy link
Copy Markdown
Contributor Author

Thanks for your information contribution @nwentling5, and for taking the time to test. Indeed, while this PR has some improvements to parsing logic and use of system prompt input for instructing JSON output rather than appending to user prompt; it still doesn't make progress towards stopping claude code's seemingly arbitrary decision to cutoff output after 4000, 8000, 10000, 12000, 16000 characters.

@nwentling5

Copy link
Copy Markdown

@ben-vargas Actually, here is my output. I thought I was on your PR, but npm was pointing to the wrong one.

CleanShot 2025-07-03 at 14 39 06@2x

Yeah. Very odd that we can't set that on the Claude side and get it to stick. Are there any other commands or settings you want me to try on my end?

@ben-vargas

Copy link
Copy Markdown
Contributor Author

Excellent research and root cause was done by @LinLL in this issue report on our repo #913 and reported to claude code here anthropics/claude-code#2904

@ben-vargas

Copy link
Copy Markdown
Contributor Author

Can some of you experiencing this issue more frequently/consistently checkout #920 and test if you get better results?

@ben-vargas

Copy link
Copy Markdown
Contributor Author

There are worthwhile fixes in here, but ultimately they were just guesses at the underlying fix for the discussion herein.

I'm closing this PR for now as I believe the root cause on the truncation issues was found and addressed in #920 - these enhancements can be done later on an updated branch, possibly as part of a refactor to use ai-sdk-provider-claude-code as a dependency rather than an internal custom-sdk implementation.

@ben-vargas ben-vargas closed this Jul 10, 2025
@ben-vargas ben-vargas deleted the fix/claude-code-append-sys-prompt branch July 31, 2025 21:48
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.

3 participants