Skip to content

fix(skills): extract first JSON object instead of last from multi-object LLM output#3978

Closed
michaelxer wants to merge 2 commits into
odysseus-dev:devfrom
michaelxer:fix-skill-extract-first-object-3965
Closed

fix(skills): extract first JSON object instead of last from multi-object LLM output#3978
michaelxer wants to merge 2 commits into
odysseus-dev:devfrom
michaelxer:fix-skill-extract-first-object-3965

Conversation

@michaelxer

Copy link
Copy Markdown
Contributor

Summary

_extract_json_object() always paired each { with the last } in the string. When the LLM emitted multiple JSON objects in one response, the first valid object was silently skipped and the later one was persisted as the skill. This caused the extractor to save the wrong skill object when the model produced trailing JSON after the intended result.

Replace the fixed-end scan with brace counting: for each { start, walk forward counting depth until we find the matching }, then try to parse that substring. This returns the first complete JSON object rather than the last.

Target branch

  • This PR targets dev, not main. All PRs land in dev; main is curated by the maintainer at each release. If your PR is on main by accident, click "Edit" on this PR and change the base.

Linked Issue

Fixes #3965

Type of Change

  • Bug fix (non-breaking — fixes a confirmed issue)

Checklist

  • I searched open issues and open PRs — this is not a duplicate.
  • This PR targets dev
  • My changes are limited to the scope described above — no unrelated refactors or whitespace changes mixed in.

How to Test

  1. Run the repro from the issue:
    from services.memory.skill_extractor import _extract_json_object
    r = _extract_json_object('{"title":"First","steps":[]} trailing {"title":"Second","steps":[]}')
    assert r == {"title": "First", "steps": []}  # should be First, not Second
  2. Verify the trailing-stray-brace case also works:
    r = _extract_json_object('{"title":"First","steps":[]} }')
    assert r == {"title": "First", "steps": []}
  3. Verify normal single-object and code-fenced inputs still work.

@michaelxer
michaelxer marked this pull request as draft June 11, 2026 18:04
@github-actions github-actions Bot added the ready for review Description complete — ready for maintainer review label Jun 11, 2026
@michaelxer
michaelxer marked this pull request as ready for review June 11, 2026 18:08
@michaelxer
michaelxer force-pushed the fix-skill-extract-first-object-3965 branch from a326a9c to 9a99a62 Compare June 11, 2026 20:08

@alteixeira20 alteixeira20 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for updating the branch. CI is green now, but this is still not ready to approve.

The PR still only changes services/memory/skill_extractor.py and does not add regression tests. The linked issue asks specifically for an end-to-end maybe_extract_skill() regression where a valid intended skill object is followed by another JSON object, to make sure the wrong skill is not persisted again.

There is also still a parser-safety concern. The current implementation uses brace counting, and the previous audit showed that this rejects valid JSON where braces appear inside string values, for example:

{"title":"Use { brace safely","steps":[]}
{"title":"Use } brace safely","steps":[]}
{"title":"Use \"quoted {\" text","steps":[]}

Please switch to a JSON-aware extraction strategy, ideally using json.JSONDecoder().raw_decode() from each candidate {, or otherwise make the scan string-aware so braces inside quoted strings and escaped quotes do not affect object depth.

Requested coverage:

  • _extract_json_object() returns the first complete object when two JSON objects are present;
  • _extract_json_object() handles a trailing stray brace after a valid first object;
  • _extract_json_object() handles braces inside JSON string values;
  • maybe_extract_skill() persists the intended first skill object when another JSON object appears after it.

After that I can re-run the final audit.

…ect LLM output

_extract_json_object() always paired each { with the last } in the
string. When the LLM emitted multiple JSON objects in one response,
the first valid object was silently skipped and the later one was
persisted as the skill.

Use brace-counting to find the matching } for each {, returning the
first complete JSON object.

Fixes odysseus-dev#3965
…N extraction

Replace brace-counting parser with raw_decode() which correctly handles
braces inside quoted string values. Add regression tests requested in
review: first-of-two-objects, trailing stray brace, braces in strings,
and end-to-end maybe_extract_skill multi-object case.
@michaelxer
michaelxer force-pushed the fix-skill-extract-first-object-3965 branch from a3ca1c9 to a360bc1 Compare June 11, 2026 22:24
@michaelxer

Copy link
Copy Markdown
Contributor Author

Pushed the regression tests in a follow-up commit covering all four cases you mentioned: first-of-two-objects, trailing stray brace, braces inside string values, and end-to-end with multi-object output. CI is green.

@alteixeira20 alteixeira20 added the bug Something isn't working label Jun 12, 2026
@michaelxer

Copy link
Copy Markdown
Contributor Author

Regression tests covering all four cases are pushed and CI is green. Ready for re-review.

@michaelxer

Copy link
Copy Markdown
Contributor Author

Closing this PR as it's been superseded by the merged fix in #3985, which handles ambiguous multi-object outputs at the memory layer. The upstream refactor of also means this branch would need a significant rebase to be relevant. Thanks for the review feedback that guided the better approach.

@michaelxer michaelxer closed this Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working ready for review Description complete — ready for maintainer review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Skill extraction can save the later JSON object from multi-object output

2 participants