feat: add JSONL draft diff logic and update command support - #1926
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## refactor #1926 +/- ##
============================================
+ Coverage 96.88% 96.95% +0.06%
============================================
Files 342 346 +4
Lines 19263 19737 +474
============================================
+ Hits 18663 19136 +473
- Misses 600 601 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
e027664 to
45eb79e
Compare
45eb79e to
e23acd1
Compare
jariy17
left a comment
There was a problem hiding this comment.
LGTM, but I would add onProgress bar
| `expected a non-empty string`, | ||
| { meta: { line: lineNumber } }, |
There was a problem hiding this comment.
I don't know if lineNumber would be useful metadata for telemetry.
| flag("id", "the ID of the dataset to update", z.string().optional()), | ||
| flag("file-path", "local JSONL file to reconcile into the DRAFT", z.string().optional()), | ||
| ], | ||
| handle: async (ctx, flags) => { |
There was a problem hiding this comment.
runDatasetExampleBatches runs batches serially and polls for ACTIVE after each one (up to 60s per batch). A large diff sits with no output for minutes, so the command looks hung. We already have the onProgress pattern in project/manager.tsx. Can we wire the batch loop into it and print "Applying update"?
There was a problem hiding this comment.
Great call out! I hooked up the onProgress pattern, and it improves the experience alot while waiting on updates to complete. PR has been updated with changes
9340e02 to
1214e97
Compare
jariy17
left a comment
There was a problem hiding this comment.
We need golden tests for update flow.
| return `${rows.map((r) => JSON.stringify(r)).join("\n")}\n`; | ||
| } | ||
|
|
||
| describe("parseJsonl", () => { |
There was a problem hiding this comment.
Do we need this tests? Will the handler tests cover this for us?
There was a problem hiding this comment.
The handler test covers the CLI routing and flag handling. It doesn't directly exercise the diff calculation and rules, which is what these tests are for.
| return { control: () => client, data: () => client, iam: () => client }; | ||
| } | ||
|
|
||
| describe("EvalClient.updateDatasetExamples", () => { |
There was a problem hiding this comment.
Do we need these unit tests? Won't the handler tests cover this?
There was a problem hiding this comment.
Same as above for handler. These tests cover the delete/update/add mutations, batching, polling logic, etc. Different surface than handlers
1214e97 to
671780e
Compare
jariy17
left a comment
There was a problem hiding this comment.
Nice job. The update diff is pretty good!
| }, | ||
| "description": "Recorded fixture for the dataset commands", | ||
| "draftStatus": "MODIFIED", | ||
| "downloadUrl": "https://agentcoredatasets685197708687-284077270265-us-west-2-an.s3.us-west-2.amazonaws.com/685197708687/datasets/agentcore_cli_dataset_fixture-jzVpQaA5It/draft/dataset.jsonl?X-Amz-Security-Token=IQoJb3JpZ2luX2VjEJP%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLXdlc3QtMiJIMEYCIQCflkdW2nGM6BMjGlmG2AV9i5D4WezB625lTpjdcrGZ6wIhAL6vK33WybFtnZ0UP%2BgXuKospslfqDD69y7%2Fzv0XXn%2BlKvIECFwQABoMMjg0MDc3MjcwMjY1IgzOAcYPrFp9BqEy2NQqzwQHK90X3idFVQP4puJ2qijfTwzzcj9xbawSUEAFYlrfw45YamqnalNNRxfK%2BeQmgA8Tj5QWWNn3noXXcQ8%2BFSwmqEkLElukt5IVBH59sUvJ13vjVUH%2Fsah%2FpCl9%2FzNs7O7rDnn0QrKQvGEHButw2ftwWobC9cijfwZWP1KXQc1hj8gLxpeNjQSH8RWAnGYsHL%2FwQVkg9AUr4Fc1sPFrdmH22Kyla62M5sJ0%2FBgB%2FsSSSqeG%2B%2FE7egRt5zCHDScu0%2FydT0GRzEkUc5TUZiB1wRi1lHRudPGfLuUGAr27Gr5RfhUvgwceu4AY6ShiBOhh8djgKD%2B7uqtgj41P4Hxg%2Ft%2FZY5PTYH9xATKM4CTyNZ6HO57xHeo2M%2BmeGIF3nLe7I53ruMq4onogm8srajiAhs1%2FPHxSSXx5ga4MwtB9pE%2BsM26JWD3QIVdu6T%2FAK5Y0CBvji2PP1jnr89nS%2B10FTYULZJn1DD7P%2F7R2idxvKDFzKY6z7tluDo3yybXSICtjapW0A72cg0vAzXrl6DKlZuZF6S9WEyKUcmDJeznwRqbSgrezL%2Bu2utpIcMdhfGsVaEvFxLuU5M9YkC7KA6rXJtn2zdm80olJN3EgOwIF%2Bf3FwBUxPCsd1F0JM4RTQC%2F4cuObwJ7hUHHZkodbAXjKPu%2B2hEGBtZZOJOB78O7liJ7xlRSaeMQl3ZETMcLKerIdlzKRRr%2BV%2F29yKHRx8TZI08Y4KoXaqrHmJi9ZaXwa8wwBwckMcrQ%2B6%2FWrWtitd6WCvzJeMwZ%2FaZEso59nRnxYtQww9sbY0wY6ogEFvsbxfePrmzBMVs4DXNp%2F3WCIpofAAPDT0lPsWqFcN8deD1qpDK4gOwprTsurb6NLaI1fjVnAFXEeYesR8WaPOvFHEIDVGh3U4uCWU9nDd4VRJFOaXGkunU5JMCfOBVCQpWU5bazXNdS3aJcr0MAhy62Ii%2BHJC5nm20JTPU%2BcEwKMxnfQVfl1b9Yr80zEWp52Y5G6DfFjb3XYuUu8HgDQCic%3D&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20260807T190246Z&X-Amz-SignedHeaders=host&X-Amz-Credential=ASIAUEJCTET4RAA4PZHK%2F20260807%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Expires=300&X-Amz-Signature=ac646581e361f5b80fce8df15655aae1cc74921c8a7658272522788555d58634", |
There was a problem hiding this comment.
Could we sanitize presigned URLs before persisting fixtures? The changed GetDataset and golden fixtures contain the full X-Amz-Security-Token, credential scope, and signature. They are short-lived, but we still should not commit them publicly. fixtureFetch already keys by the stable pathname, so would it make sense to store a redacted copy while returning the real URL during recording, and redact the corresponding golden output?
| try { | ||
| // The remote request has already succeeded, so checkpoint its IDs even | ||
| // if cancellation arrives before the next poll or batch. | ||
| await atomicWrite(filePath, nextLocalText); |
There was a problem hiding this comment.
I was wondering if we need an optimistic check before replacing the local file here. The command may run for several minutes, but each checkpoint is rebuilt from the initial localExamples snapshot. I reproduced editing the JSONL while the Add request was running, and this write silently replaced that edit with the original row plus its assigned ID. Could we verify that the file still matches the last known contents before replacing it, and preserve the reconciled output separately if it changed?
There was a problem hiding this comment.
Yeah adding this check makes total sense. I've updated the logic so we verify the expected state of the local file. If the file has changed during update we leave it untouched. Instead, we write the reconciled output to a separate recovery file, surface that files' path to the user, and stop before the next batch.
| // Build every batch before mutating the remote draft so an oversized | ||
| // individual example cannot fail after earlier phases have already run. | ||
| const deleteBatches = buildDatasetExampleBatches({ | ||
| items: diff.deleteIds, | ||
| payloadItem: (exampleId) => exampleId, | ||
| requestBody: (exampleIds, clientToken) => ({ exampleIds, clientToken }), | ||
| }); |
There was a problem hiding this comment.
Should the payload-size calculation include datasetId? These requestBody callbacks size only the examples and client token, while the actual SDK commands also include datasetId. I constructed a batch accepted as exactly 5 MB here whose actual command input was 115 bytes over the limit. Would it make sense to size the complete command input for all three mutation types so the preflight guarantee is accurate?
There was a problem hiding this comment.
Great catch; absolutely should factor datasetId into the calc. Updated
| const path = fixturePath(dir, command); | ||
|
|
||
| if (isRecording()) { | ||
| const shouldWrite = !recordedPaths.has(path); |
There was a problem hiding this comment.
Could this first-response behavior break existing record/replay flows that poll the same request? For example, the Harness fixture records repeated GetHarness calls and relies on the final READY response being left in the fixture. With this global first-write rule, a fresh RECORD=1 run can preserve the initial CREATING response, and the next offline replay fails because the fixture is not settled. Would it make sense to support response sequences or scope this behavior to the Dataset update fixture?
There was a problem hiding this comment.
Good callout. I reset the last-response behavior in the shared recorder. Also moved the update fixture into its own directory, which prevents overwriting fixtures from the get fixture. This fixes the initial issue which made me switch to first reponse behavior originally
671780e to
41bd1a1
Compare
d7ff900 to
ebdc34e
Compare
3e08721 to
4203c52
Compare
4203c52 to
934b59a
Compare
Description
Adds
eval dataset update, which reconciles a local JSONL draft file into the remote datasetDRAFT. Update is append-only at the service level, so the CLI cannot replace the JSONL file wholesale. Instead, we have to compute a diff between the local draft and the remote draft, then apply the required dataset mutations:exampleIdexists remotely but has content changesSummary of changes:
eval dataset update --id <dataset-id> --file-path <path>.DRAFTthrough the presigneddownloadUrlfromGetDataset. I considered using the ListDatasetExamples API for this, but this would've required N API calls, where N isceil(exampleCount / pageSize).exampleIdfor newly added examples back into the local JSONL file, so future updates reconcile correctly.exampleIdas non-empty strings, and rejects duplicateexampleIdexampleId's are unique and responses have one ID per exampleeval dataset get --file-pathand update’s remote draft fetchRelated Issue
Closes #
Documentation PR
Type of Change
Testing
Manually tested commands using
bun run ...against personal account with following procedure:eval dataset udpateadded: 1,updated: 1,deleted: 1,unchanged: 0exampleIdFull suite against latest HEAD after rebasing onto refactor:
How have you tested the change?
npm run test:unitandnpm run test:integnpm run typechecknpm run lintsrc/assets/, I rannpm run test:update-snapshotsand committed the updated snapshotsChecklist
Stack created with GitHub Stacks CLI • Give Feedback 💬