crouter
SDK

Release notes

Behavior changes in @crouter/sdk that an app must act on, newest first.

Changes to @crouter/sdk that can change what an existing app sees. Newest first.

Next release (after 0.3.372)

CrouterError.code is typed SdkErrorCode

Every code the SDK raises itself is now listed in sdkErrorCodes (exported with SdkErrorCode and mapError from @crouter/sdk/errors), and CrouterError's code and constructor are typed with it. Runtime behavior is unchanged. An app that constructs new CrouterError(message, code) with a code of its own, for example in a test double, no longer typechecks: throw its own error class or an APIError instead. See Errors.

Run ergonomics (additive)

runs.start({prompt, message}) stores a first message that always joins the first turn, and returns it as run.message for runs.reply. RunReply.collect({onDelta}) streams a reply and says how its turn ended; text() takes the same onDelta. runs.listAll and memory.listAll iterate every page. refreshRefusal(error) classifies a refused refresh, and each refreshIdleConnections failure carries it as reason. authorizeUrl takes prompt: 'consent'. parsePrivateJwk and privateKeyFromFile load the app's signing key. No existing call changes.

runs.get reports what a running run is doing (additive)

RunObject.activity is {phase: 'starting' | 'thinking' | 'writing' | 'between_turns'} or {phase: 'tool', tool: {name, summary}} while the run is running, and null otherwise, so a polling app can show progress from runs.get without reading runs.trace. runs.list entries carry it too. See Showing a background run's progress. A test double that builds a RunObject literal must add activity.

0.3.371 (after 0.3.370)

A refused refresh says why: grant_removed, grant_suspended, refresh_token_expired

When the directory refuses a connection's refresh token, the SDK now raises the directory's reason as its own code instead of grant_revoked:

  • grant_removed: the person removed your app or deleted their account, and the runtime deleted your app's data for them. This is the only code on which you may delete what you keep for that person.
  • grant_suspended: your app is paused for that person. Nothing was deleted; keep their data.
  • refresh_token_expired: the refresh token went 90 days unused. The grant is still active; reconnecting skips the consent page.

Each is a 401 AuthenticationError with retryable: false, userAction: 'reconnect', and origin: 'directory', and the connected client from oauth.client(...) stops refreshing after it, as it already did for grant_revoked and refresh_token_reused. errorCodes and the ErrorCode type gain the three codes. The table of codes and app actions is in When a refresh is refused.

Behavior changes for existing apps:

  • If your app deletes a person's data on grant_revoked, switch it to grant_removed. A removal no longer arrives as grant_revoked. grant_revoked stays (it is not deprecated) and now means only that the directory or runtime refused without naming a reason; never delete data on it.
  • grant_revoked from the token endpoint is now a 401 AuthenticationError, not a 400 BadRequestError. It previously carried the directory's HTTP 400 status; it now has status 401 like the rest of contract 2's grant_revoked. Code that checks error instanceof BadRequestError or error.status === 400 for a refused refresh must check error.code, or error.userAction === 'reconnect', instead.
  • If your app reconnects on grant_revoked, switch on userAction === 'reconnect': every refused-refresh code carries it.

An SDK older than this release maps the three new reasons to grant_revoked, so an app keeps its current behavior until it upgrades.

On this page