Skip to content

fix(ts): report correct paths for values after nested objects in deepCopyWithValidation - #4981

Merged
opieter-aws merged 2 commits into
strands-agents:mainfrom
BlueX888:fix/prep-deepcopy-validation-stale-pathstack
Oct 19, 2026
Merged

opieter-aws merged 2 commits into
strands-agents:mainfrom
BlueX888:fix/prep-deepcopy-validation-stale-pathstack

Conversation

@BlueX888

@BlueX888 BlueX888 commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Human Overview

Description

deepCopyWithValidation reports the wrong path for any non-serializable value that comes after a nested object or array. The replacer's path stack (strands-ts/src/types/json.ts:74) is pushed on every descent but never popped, so once a nested subtree finishes serialization the stale frame stays on top and every later sibling inherits that finished subtree's path.

Users hit this through StateStore state validation (strands-ts/src/state-store.ts:92) and router metadata validation (strands-ts/src/models/routing/router.ts:494): storing { nested: { x: 1 }, bad: fn } under key a is reported as value for key "a".nested.bad instead of value for key "a".bad, pointing users at the wrong location. The existing tests only covered single-descent paths, so the sibling case was untested.

Each stack frame now carries the object it was pushed for, and the replacer pops frames down to the object currently being serialized, so paths always resolve against the actual parent.

Related Issues

None — small bug fix, no prior issue.

Documentation PR

Not needed — no public API change, only corrected error-message paths.

Type of Change

Bug fix

Testing

  • I ran hatch run prepare

Not run: this PR touches only strands-ts/; the hatch prepare pipeline covers strands-py/. The TypeScript gates all pass:

  • npm test — 175 test files, 4891 tests passed
  • pre-commit gate (build, test:coverage, lint, format:check, type-check) — all passed
  • New regression tests in src/types/__tests__/json.test.ts fail on main with the stale paths (value for key "a".nested.bad, items[0][1], value.first.deep.second.deep.bad) and pass with this change

Additional Details

Before / after
deepCopyWithValidation({ ok: 1, nested: { x: 1 }, bad: fn }, 'value')
// before: JsonValidationError: value.nested.bad contains a function which cannot be serialized
// after:  JsonValidationError: value.bad contains a function which cannot be serialized

deepCopyWithValidation([{ x: 1 }, fn], 'items')
// before: JsonValidationError: items[0][1] contains a function which cannot be serialized
// after:  JsonValidationError: items[1] contains a function which cannot be serialized

Checklist

  • I have read the CONTRIBUTING document
  • I have reviewed and understand every line of code in this PR, including any generated by AI tools, and I can explain why it works
  • My change is focused and reasonably small; I have split unrelated work into separate PRs
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@BlueX888
BlueX888 requested a review from a team as a code owner October 7, 2026 19:22
@BlueX888
BlueX888 requested a review from opieter-aws October 7, 2026 19:22
@github-actions github-actions Bot added area-devx Developer experience improvements typescript Pull requests that update typescript code bug Something isn't working labels Oct 7, 2026
@github-actions github-actions Bot added strands-running complexity/high A touched function exceeds cognitive complexity 25; may be worth splitting size/xs labels Oct 16, 2026
Comment thread strands-ts/src/types/json.ts Resolved
@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Approve

Clean, well-scoped fix for the stale pathStack frame bug. The identity-based approach (matching the current parent via this and popping finished subtrees) is correct, and I traced the sibling, array, multi-level, and shared-reference cases to confirm paths resolve against the real parent. Node unit tests pass locally (42 tests); the browser-project failure is an unrelated missing-Playwright issue.

Review Categories
  • Correctness: Fix is sound; arrow→function conversion and this: object typing are necessary and correctly applied.
  • API: deepCopyWithValidation is internal (not exported from index.ts) and its signature is unchanged — no public API impact. The corrected paths do surface to users via StateStore/router validation, so the fix is valuable.
  • Testing: Good regression coverage. One suggestion to also cover a value nested inside a later sibling (pop-then-redescend) — non-blocking.
  • Scope: ~46 lines, tightly focused. Noted one pre-existing numeric-key edge case as a potential follow-up only.

Nice job pinpointing the root cause and documenting the before/after behavior so clearly.

Comment thread strands-ts/src/types/json.ts Resolved
@opieter-aws

Copy link
Copy Markdown
Contributor

Thanks for the fix! Using this to find the parent object resolves the reported bug nicely. One request inline: two older wrong-path cases are on the lines this PR rewrites and are easy to fix now, could you please include them?

…CopyWithValidation

The replacer's path stack was pushed for every object/array but never
popped, so after a nested subtree finished the stale frame stayed on top
and every later sibling was reported under the wrong parent (e.g.
`value.nested.bad` instead of `value.bad`, `items[0][1]` instead of
`items[1]`).

Pop frames for finished subtrees by comparing the replacer's holder
(`this`) with the frame's object, so paths resolve against the object
currently being serialized.
…validation

The bracket-notation decision looked only at the key, so numeric-string
keys on plain objects were reported as array indices (`{ '0': fn }` as
`value[0]` instead of `value.0`), and the root check `key !== ''` also
swallowed a real empty-string property, hiding its segment from the path
(`{ '': { bad: fn } }` as `value.bad` instead of `value[''].bad`).

Choose bracket notation only when the holder (`this`) is an array, and
distinguish the initial replacer call (empty key, no frames on the
stack) from a real '' property key, which is appended as `['']`.
@BlueX888
BlueX888 force-pushed the fix/prep-deepcopy-validation-stale-pathstack branch from 06b4d2f to 1273ef3 Compare October 17, 2026 01:17
@BlueX888

Copy link
Copy Markdown
Contributor Author

Thanks @opieter-aws for the review, and glad the parent-tracking approach resolves the reported bug! Quick heads-up: upstream history was restarted, so I rebased this branch onto current main (cf0c6aee); the same two changes are unchanged, only their hashes moved. Both older cases you flagged are now fixed in 1273ef3 (fix(ts): correct paths for numeric and empty-string keys in deepCopy validation), each with its own regression test.

1. Numeric-looking keys on plain objects

The bracket-notation decision now checks the holder (this) rather than the key alone, so brackets are used only for real arrays:

  • { '0': fn }: previously reported value[0], now value.0
  • { '42': fn }: previously reported value[42], now value.42
  • array elements still use bracket notation (e.g. items[1]), unchanged

2. Real empty-string keys

The root check now distinguishes the initial replacer call (empty key, no frames on the stack) from a real '' property key, which is visited while its parent object is on the stack. A real empty-string key is appended as [''], so it can no longer collapse into the root:

  • { '': { bad: fn } }: previously reported value.bad, now value[''].bad

Tests

src/types/__tests__/json.test.ts gained two cases covering both reports, and the full file passes:

vitest run --project unit-node src/types/__tests__/json.test.ts

Test Files  1 passed (1)
      Tests  44 passed (44)

The original stale-pathStack tests (sibling keys, array elements, and keys after multiple levels of nesting) all still pass, as do the StateStore tests that assert paths from the same validation code.

@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Approve

Re-reviewed after the rebase and 1273ef3. The follow-up cleanly resolves both the numeric-key path and the empty-string-key path, and @opieter-aws's request is fully addressed.

Verification
  • Code: Bracket notation now keyed off Array.isArray(this) (not a key regex), and the isRoot = key === '' && parentFrame === undefined check correctly separates the synthetic root call from a genuine '' property.
  • Runtime-checked all four shapes resolve correctly: { a:{x:1}, b:{y:fn} } → value.b.y, { '0': fn } → value.0, { '': { bad: fn } } → value[''].bad, [{x:1}, fn] → items[1].
  • Tests: json.test.ts 44/44 pass (two new regression cases); dependent state-store.test.ts 49/49 pass, so the user-facing validation paths stay consistent.

Thanks for the thorough follow-up and the extra regression coverage.

@opieter-aws
opieter-aws merged commit bd0b6a9 into strands-agents:main Oct 19, 2026
26 checks passed
gautamsirdeshmukh pushed a commit to gautamsirdeshmukh/harness-sdk that referenced this pull request Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-devx Developer experience improvements bug Something isn't working complexity/high A touched function exceeds cognitive complexity 25; may be worth splitting size/s typescript Pull requests that update typescript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants