Repository navigation
feat(cli): continue cut-off responses with a declared continuation recipe - #142
Open
TristanSpeakEasy wants to merge 23 commits into
Open
TristanSpeakEasy wants to merge 23 commits into
TristanSpeakEasy wants to merge 23 commits into
Conversation
Some APIs report that a request failed inside a successful HTTP response: a 200 whose body carries a terminal status such as "failed" or "cancelled", often with an errors[] array, in both the discrete JSON response and the completion event of an SSE stream. A generated CLI only knew the HTTP outcome, so it printed the payload and exited 0. x-speakeasy-cli-commands gains output.outcome on commands and operation entries: statePointer (state in the JSON response), streamStatePointer (state inside streamed events), a states table using the async classifications, and optional errorPointer / streamErrorPointer / errorMessageField for the failure detail. The decoder requires a state pointer and at least one failure classification; the linker requires the JSON state pointer to select a closed string enum that the table classifies completely, checks the stream pointer like a stream projection, and rejects an outcome table beside an async recipe. At runtime the table travels on a speakeasy_outcome annotation. Discrete responses are rendered as before and then, when the state classifies as failure, the command returns CLI_OUTCOME_FAILED (runtime_error, exit 1) naming the state and the first failure-detail message; artifact commands skip the media write. Streams render every event and report metadata before returning the same error. Only "failure" changes behaviour; other classifications, unknown states, events without the state, and streams with no completion event keep exit 0. Undeclared commands are unchanged. The review CLI declares a discrete table on render and a stream table on say; the primary CLI overlay adds a CLI-only interaction lookup and stream with tests across JSON, pretty and agent mode.
…rrorPointer Paginated commands rendered pages through PaginatedResult without ever consulting the declared statePointer, so --all exited 0 on a page whose body carried a failure state. Each page is now classified after it is rendered and the command fails on the first page in a failure state. The linker also walks streamErrorPointer through the streaming event shapes, requiring an object or array in at least one of them, so a path that resolves nowhere is rejected at generation time instead of silently dropping the failure detail.
…s once The JSON state and error pointers now go through one response-path walker and accept nullable leaves: a null union arm or a "null" entry in a type list is treated as absence rather than rejected. Each streamed event is decoded once and the frame is shared between outcome classification and the wire rendering. Narrative comments are dropped from the outcome templates so they no longer land in generated CLIs.
…ept failure detail in any response variant The linker now collects the closed enum a streamStatePointer resolves to and holds the states table to it: a stream-only table must name exactly those members, and when statePointer already pinned the table to the JSON enum every stream member must still be classified. A leaf without a closed enum warns that the table cannot be verified; an open enum is rejected. errorPointer no longer has to resolve in every response variant. Failure detail that only exists on the failed arm of a union is accepted, and the pointer is rejected only when it resolves in no variant of any JSON 2xx response, with a did-you-mean from the properties it passed. The review fixture's streamed status gains an enum so the regenerated review CLI exercises the stream-states check. Co-Authored-By: Claude <noreply@anthropic.com>
…stream frames on demand, and cover the artifact and single-page outcome paths A single-route intent without its own output.outcome now inherits the table its operation entry declared, so a state classified for the operation command also fails the intent; async intents keep async.states as their only classification. The primary fixture's outcome-check command drops its duplicate table and relies on the inheritance. Streams decode an event's wire frame only when an outcome table needs it or the wire re-encode will use it, restoring the previous per-event cost for projected and table output. The unused StateSegments field leaves the outcome model and the unreachable string arm leaves the failure-detail reader. The generated artifact harness gains a case where the response carries the command's declared failure state and asserts nothing is written, and the paginated test now serves a configurable first-page status so a failed first page is shown to exit 1 without --all. Co-Authored-By: Claude <noreply@anthropic.com>
…t outcome fixture, and document the outcome envelope fields Failure detail arrays of plain strings now contribute their first entry to the CLI_OUTCOME_FAILED message. The generated artifact failure fixture builds array elements for numeric pointer segments and Go-quotes the asserted state. The agent README names the state and error fields the outcome envelope adds. The states map is built on a null prototype so any enum member name survives serialization. perms.go and the review CLI are regenerated.
…cipe Commands whose operation declares x-speakeasy-cli-commands continuation re-send the request with resume values taken from the response while output.outcome classifies its state as continue. Discrete responses are combined (merge arrays, sum numbers); streamed responses are written as one output across hops. Adds --auto-continue, --max-continuations and the CLI_CONTINUATION_LIMIT reason.
…ponses and missing tokens A stream that ends in a pending state while holding resume values is reported as a connection error, a JSON answer from an operation that can also stream continues as a discrete response, and a continue state without its required resume value is a CLI_PROTOCOL error in every output mode.
There was a problem hiding this comment.
All reported issues were addressed across 18 files
Shadow auto-approve: would not auto-approve because issues were found.
View guided diff | Turn on auto-fix | Re-trigger cubic
Reject a continue outcome state that no continuation recipe acts on, accept continuation targets declared beside a request-body union, sum merged numbers exactly, keep hop context on cancellation, and read transformed responses the way the single-request path does.
There was a problem hiding this comment.
0 issues found across 8 files (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would require human review. Adds an opt-in continuation recipe that makes generated CLIs re-send requests with resume values and combine or stream results, with new flags, error reasons, and a streaming refactor. The auto-retry default/hop limit and combined-output contract need human sign-off.
View guided diff | Turn on auto-fix | Re-trigger cubic
A nullable async.statePointer leaf now passes the response-path type check, so its null enum member must be skipped the same way the output.outcome linker skips it instead of failing as a non-string member. Addresses review feedback on the async state enum loop.
Operations with a binary 2xx response wrote --output-file and --output-b64 output before output.Result ran, so a JSON response in a declared failure state was saved or printed and the command exited 0. Both paths now classify the response, write it as before, and then return CLI_OUTCOME_FAILED. The primary CLI fixture gains a mixed JSON/binary operation covering both flags. Addresses review feedback on the output-file and output-b64 early returns.
The stream path walker followed a $ref straight to its component when segments remained, dropping sibling keywords such as properties. A narrowed nested state was then checked against the referenced component's full enum. The walker now rewrites a sibling-constrained reference as an implicit allOf, sharing the rewrite resolveObjectSchema already used, and guards the rewrite against reference cycles. Addresses review feedback on $ref siblings in the stream walker.
# Conflicts: # internal/extensions/cli_commands_test.go # zSDKs/sdk-cli/.speakeasy/gen.lock
…tates A oneOf or anyOf beside a closed enum was dropped whenever one of its arms was not closed, so the leaf fell back to the unverifiable warning and a misspelled states table was accepted. The union now filters the members the other keywords already pin instead of discarding them. oneOf arms were also combined as an ordinary union, so a member that matches more than one arm, which oneOf rejects, still had to be classified. When every arm is closed, oneOf now keeps only members that match exactly one arm; with an arm that is not closed it requires a match, keeping every possible member in the table.
…istan/cli-continuation # Conflicts: # internal/extensions/cli_commands_test.go # templates/templates/cli/auxiliary/internal/output/streaming.go.stmpl # zSDKs/sdk-cli/.speakeasy/gen.lock # zSDKs/sdk-cli/internal/output/streaming.go
# Conflicts: # zSDKs/sdk-cli/.speakeasy/gen.lock
…istan/cli-continuation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Some APIs stop a response early and expect the client to send the same request again with a resume value from the response to get the rest. Each response reports a state such as
more_requiredand carries a token, and only returns its own part of the output (its own steps, its own usage). Generated CLIs currently print the first part and exit 0, so users and scripts get partial output with no indication that more was available.Stacked on #134, which adds
output.outcomestate tables. This PR adds acontinuestate class and acontinuation:recipe that drives the loop.What changed
New
continuation:recipe onx-speakeasy-cli-commandscommands and operation entries. Single-route intent commands inherit it from their operation entry, the same wayoutput.outcomeis inherited.streamFromreads bindings from streamed events instead. Operations with both JSON and streaming responses declare both.Generation-time validation:
continue, with a state pointer for every response kind the operation has.fromandstreamFrommust resolve to strings, andtomust be a writable string in every request-body variant (including properties declared beside a union'soneOf/anyOf).continuestate on a command or operation entry without a continuation recipe is rejected, since that command would otherwise exit 0 on a response that says it is incomplete.mergetargets must be arrays andsumtargets numbers.Runtime:
mergeandsum(summed exactly, so totals beyondint64stay correct); every other field comes from the last response. Streamed events from every hop go to one output, through one stream projector.Flags:
--auto-continue(default from the recipe) and--max-continuations(default 32, or the recipe'smaxHops).Stop conditions:
CLI_CONTINUATION_LIMIT(exit 1). The envelope carrieshops,continuation(the resume values), and a hint naming the command's own flags.CLI_OUTCOME_FAILED, also carryinghopsandcontinuation.continuestate without its required resume value:CLI_PROTOCOL(exit 1). The output is printed first.pendingstate, while holding resume values:CLI_CONNECTION, with the resume values. Cancellation carries the same hop context.Shared streaming refactor:
StreamResultnow drains one response at a time so a hop driver can chain the next stream. CLIs without a recipe behave the same; the review fixture diff inzSDKs/sdk-cliis this refactor plus the new reason constant. The continuation runtime file is only generated when a recipe is declared.Review notes
output/continuation.go.stmplholds the loop. The parts worth the closest look:setContinuationBodyValue: writes a resume value into the typed request through a JSON round trip of the request-body field.streamContinuation.next: decides whether a stream ended cleanly, dropped, or should continue.streamResultintodiscrete.continuestate class lives in theoutput.outcomestate table from feat(cli): fail commands on a declared terminal failure state (output.outcome) #134 and is accepted only by commands that declare a continuation. Async commands still reject it.Testing
go test ./internal/extensions/: passed. Covers decode and link, the nestedtotarget, atoshared by union members, inheritance, and about 26 rejection cases.continuation_additional_test.go.stmpl, served by in-test HTTP servers that record each hop's request body:int64--auto-continue=false--max-continuations 0TARGET=primary make test-cli(2250 tests) andTARGET=review make test-cli(759 tests): passed.make check-templates(all targets, sincetypeDefs.d.tschanged),go generate ./...,npm run format,go mod tidy -diff, the CLI snapshot tests, and staticcheck on the generated CLI: passed.Public-safety check
.claude/skills/public-repo-communication/SKILL.md).git diff --check.