Skip to content

Commit 2ffb5a3

Browse files
committed
feat(common): Add PromptResult<T> type for explicit abort handling
- Introduce PromptResult<T> discriminated union (PromptSuccess<T> | PromptAborted) - Add helper functions: promptSuccess, promptAborted, isAbortError, unwrapPromptResult - Add ABORT_ERROR_MESSAGE constant for consistent error messages - Update LLM contract types to return PromptResult
1 parent 78d8acf commit 2ffb5a3

File tree

2 files changed

+124
-3
lines changed

2 files changed

+124
-3
lines changed

common/src/types/contracts/llm.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { ParamsExcluding } from '../function-params'
55
import type { Logger } from './logger'
66
import type { Model } from '../../old-constants'
77
import type { Message } from '../messages/codebuff-message'
8+
import type { PromptResult } from '../../util/error'
89
import type { generateText, streamText, ToolCallPart } from 'ai'
910
import type z from 'zod/v4'
1011

@@ -52,7 +53,7 @@ export type PromptAiSdkStreamFn = (
5253
trackEvent: TrackEventFn
5354
signal: AbortSignal
5455
} & ParamsExcluding<typeof streamText, 'model' | 'messages'>,
55-
) => AsyncGenerator<StreamChunk, string | null>
56+
) => AsyncGenerator<StreamChunk, PromptResult<string | null>>
5657

5758
export type PromptAiSdkFn = (
5859
params: {
@@ -78,7 +79,7 @@ export type PromptAiSdkFn = (
7879
n?: number
7980
signal: AbortSignal
8081
} & ParamsExcluding<typeof generateText, 'model' | 'messages'>,
81-
) => Promise<string>
82+
) => Promise<PromptResult<string>>
8283

8384
export type PromptAiSdkStructuredInput<T> = {
8485
apiKey: string
@@ -104,7 +105,7 @@ export type PromptAiSdkStructuredInput<T> = {
104105
trackEvent: TrackEventFn
105106
signal: AbortSignal
106107
}
107-
export type PromptAiSdkStructuredOutput<T> = Promise<T>
108+
export type PromptAiSdkStructuredOutput<T> = Promise<PromptResult<T>>
108109
export type PromptAiSdkStructuredFn = <T>(
109110
params: PromptAiSdkStructuredInput<T>,
110111
) => PromptAiSdkStructuredOutput<T>

common/src/util/error.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,60 @@ export type Failure<E extends ErrorObject = ErrorObject> = {
1212
error: E
1313
}
1414

15+
/**
16+
* Result type for prompt functions that can be aborted.
17+
* Provides rich semantics to distinguish between successful completion and user abort.
18+
*
19+
* ## When to use `PromptResult<T>` vs `ErrorOr<T>`
20+
*
21+
* Use `PromptResult<T>` when:
22+
* - The operation can be cancelled by the user (via AbortSignal)
23+
* - An abort is an expected outcome, not an error
24+
* - You need to distinguish between errors (which might trigger fallbacks) and
25+
* user-initiated aborts (which should propagate immediately)
26+
*
27+
* Use `ErrorOr<T>` when:
28+
* - The operation can fail with an error that should be handled
29+
* - There's no concept of user-initiated abort
30+
* - You want to return error details rather than throw
31+
*
32+
* ## Abort handling patterns
33+
*
34+
* 1. **Check and return early** - For graceful handling where abort means "stop, no error":
35+
* ```ts
36+
* const result = await promptAiSdk({ ... })
37+
* if (result.aborted) return // or return null, false, etc.
38+
* doSomething(result.value)
39+
* ```
40+
*
41+
* 2. **Unwrap and throw** - For propagating aborts as exceptions:
42+
* ```ts
43+
* const value = unwrapPromptResult(await promptAiSdk({ ... }))
44+
* // Throws if aborted, callers should use isAbortError() in catch blocks
45+
* ```
46+
*
47+
* 3. **Rethrow in catch blocks** - Prevent swallowing abort errors:
48+
* ```ts
49+
* try {
50+
* await someOperation()
51+
* } catch (error) {
52+
* if (isAbortError(error)) throw error // Don't swallow aborts
53+
* // Handle other errors
54+
* }
55+
* ```
56+
*/
57+
export type PromptResult<T> = PromptSuccess<T> | PromptAborted
58+
59+
export type PromptSuccess<T> = {
60+
aborted: false
61+
value: T
62+
}
63+
64+
export type PromptAborted = {
65+
aborted: true
66+
reason?: string
67+
}
68+
1569
export type ErrorObject = {
1670
name: string
1771
message: string
@@ -50,6 +104,72 @@ export function failure(error: unknown): Failure<ErrorObject> {
50104
}
51105
}
52106

107+
/**
108+
* Create a successful prompt result.
109+
*/
110+
export function promptSuccess<T>(value: T): PromptSuccess<T> {
111+
return {
112+
aborted: false,
113+
value,
114+
}
115+
}
116+
117+
/**
118+
* Create an aborted prompt result.
119+
*/
120+
export function promptAborted(reason?: string): PromptAborted {
121+
return {
122+
aborted: true,
123+
...(reason !== undefined && { reason }),
124+
}
125+
}
126+
127+
/**
128+
* Standard error message for aborted requests.
129+
* Use this constant when throwing abort errors to ensure consistency.
130+
*/
131+
export const ABORT_ERROR_MESSAGE = 'Request aborted'
132+
133+
/**
134+
* Check if an error is an abort error.
135+
* Use this helper to detect abort errors in catch blocks.
136+
*
137+
* Detects both:
138+
* - Errors with message 'Request aborted' (thrown by our code via ABORT_ERROR_MESSAGE)
139+
* - Native AbortError (thrown by fetch/AI SDK when AbortSignal is triggered)
140+
*/
141+
export function isAbortError(error: unknown): boolean {
142+
if (!(error instanceof Error)) {
143+
return false
144+
}
145+
// Check for our custom abort error message
146+
if (error.message === ABORT_ERROR_MESSAGE) {
147+
return true
148+
}
149+
// Check for native AbortError (DOMException or Error with name 'AbortError')
150+
// This is thrown by fetch, AI SDK, and other web APIs when AbortSignal is triggered
151+
if (error.name === 'AbortError') {
152+
return true
153+
}
154+
return false
155+
}
156+
157+
/**
158+
* Unwrap a PromptResult, returning the value if successful or throwing if aborted.
159+
*
160+
* Use this helper for consistent abort handling when you want aborts to propagate
161+
* as exceptions. Callers should use `isAbortError()` in catch blocks to detect
162+
* and handle abort errors appropriately (e.g., rethrow instead of logging as errors).
163+
*
164+
* @throws {Error} When result.aborted is true. The error message is ABORT_ERROR_MESSAGE.
165+
*/
166+
export function unwrapPromptResult<T>(result: PromptResult<T>): T {
167+
if (result.aborted) {
168+
throw new Error(ABORT_ERROR_MESSAGE)
169+
}
170+
return result.value
171+
}
172+
53173
// Extended error properties that various libraries add to Error objects
54174
interface ExtendedErrorProperties {
55175
status?: number

0 commit comments

Comments
 (0)