@proposit/proposit-core 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/cli/commands/parse.d.ts.map +1 -1
  2. package/dist/cli/commands/parse.js +10 -11
  3. package/dist/cli/commands/parse.js.map +1 -1
  4. package/dist/cli/commands/repair.js +1 -1
  5. package/dist/cli/commands/repair.js.map +1 -1
  6. package/dist/cli/commands/validate.js +1 -1
  7. package/dist/cli/commands/validate.js.map +1 -1
  8. package/dist/cli/engine.js +4 -4
  9. package/dist/cli/engine.js.map +1 -1
  10. package/dist/cli/import.js +1 -1
  11. package/dist/cli/import.js.map +1 -1
  12. package/dist/cli/llm/index.js +2 -2
  13. package/dist/cli/llm/index.js.map +1 -1
  14. package/dist/extensions/argument-ingestion/shared/basics-extension.js +3 -3
  15. package/dist/extensions/argument-ingestion/shared/basics-extension.js.map +1 -1
  16. package/dist/extensions/argument-ingestion/shared/finalize-response-v2.d.ts.map +1 -1
  17. package/dist/extensions/argument-ingestion/shared/finalize-response-v2.js +1 -2
  18. package/dist/extensions/argument-ingestion/shared/finalize-response-v2.js.map +1 -1
  19. package/dist/extensions/argument-ingestion/shared/types.d.ts +2 -2
  20. package/dist/extensions/argument-ingestion/stages/conclusion-selection.d.ts +12 -13
  21. package/dist/extensions/argument-ingestion/stages/conclusion-selection.d.ts.map +1 -1
  22. package/dist/extensions/argument-ingestion/stages/conclusion-selection.js +110 -39
  23. package/dist/extensions/argument-ingestion/stages/conclusion-selection.js.map +1 -1
  24. package/dist/extensions/argument-ingestion/stages/schemas.d.ts +6 -0
  25. package/dist/extensions/argument-ingestion/stages/schemas.d.ts.map +1 -1
  26. package/dist/extensions/argument-ingestion/stages/schemas.js +14 -0
  27. package/dist/extensions/argument-ingestion/stages/schemas.js.map +1 -1
  28. package/dist/extensions/argument-ingestion/v1-single-shot.d.ts.map +1 -1
  29. package/dist/extensions/argument-ingestion/v1-single-shot.js +13 -13
  30. package/dist/extensions/argument-ingestion/v1-single-shot.js.map +1 -1
  31. package/dist/extensions/openai/errors.d.ts +23 -0
  32. package/dist/extensions/openai/errors.d.ts.map +1 -1
  33. package/dist/extensions/openai/errors.js +28 -0
  34. package/dist/extensions/openai/errors.js.map +1 -1
  35. package/dist/extensions/openai/index.d.ts +3 -3
  36. package/dist/extensions/openai/index.d.ts.map +1 -1
  37. package/dist/extensions/openai/index.js +4 -3
  38. package/dist/extensions/openai/index.js.map +1 -1
  39. package/dist/extensions/openai/provider.d.ts +118 -0
  40. package/dist/extensions/openai/provider.d.ts.map +1 -1
  41. package/dist/extensions/openai/provider.js +272 -23
  42. package/dist/extensions/openai/provider.js.map +1 -1
  43. package/dist/extensions/openai/structured-output.js +3 -3
  44. package/dist/extensions/openai/structured-output.js.map +1 -1
  45. package/dist/lib/core/argument-engine.d.ts +7 -8
  46. package/dist/lib/core/argument-engine.d.ts.map +1 -1
  47. package/dist/lib/core/argument-engine.js +36 -39
  48. package/dist/lib/core/argument-engine.js.map +1 -1
  49. package/dist/lib/core/expression-manager.d.ts +6 -6
  50. package/dist/lib/core/expression-manager.d.ts.map +1 -1
  51. package/dist/lib/core/expression-manager.js +35 -42
  52. package/dist/lib/core/expression-manager.js.map +1 -1
  53. package/dist/lib/core/fork-library.d.ts +1 -1
  54. package/dist/lib/core/fork-library.js +1 -1
  55. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts +5 -7
  56. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts.map +1 -1
  57. package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts +6 -6
  58. package/dist/lib/core/premise-engine.js +9 -9
  59. package/dist/lib/core/premise-engine.js.map +1 -1
  60. package/dist/lib/grammar/an-rules.d.ts +24 -33
  61. package/dist/lib/grammar/an-rules.d.ts.map +1 -1
  62. package/dist/lib/grammar/an-rules.js +83 -114
  63. package/dist/lib/grammar/an-rules.js.map +1 -1
  64. package/dist/lib/grammar/auto-normalize.d.ts.map +1 -1
  65. package/dist/lib/grammar/auto-normalize.js +5 -6
  66. package/dist/lib/grammar/auto-normalize.js.map +1 -1
  67. package/dist/lib/grammar/bounded-subtree.d.ts.map +1 -1
  68. package/dist/lib/grammar/bounded-subtree.js +2 -3
  69. package/dist/lib/grammar/bounded-subtree.js.map +1 -1
  70. package/dist/lib/grammar/naked-q.js +4 -4
  71. package/dist/lib/grammar/naked-q.js.map +1 -1
  72. package/dist/lib/grammar/normalize.js +4 -4
  73. package/dist/lib/grammar/normalize.js.map +1 -1
  74. package/dist/lib/grammar/populate-from.d.ts.map +1 -1
  75. package/dist/lib/grammar/populate-from.js +3 -4
  76. package/dist/lib/grammar/populate-from.js.map +1 -1
  77. package/dist/lib/index.d.ts.map +1 -1
  78. package/dist/lib/index.js +1 -2
  79. package/dist/lib/index.js.map +1 -1
  80. package/dist/lib/llm/types.d.ts +15 -0
  81. package/dist/lib/llm/types.d.ts.map +1 -1
  82. package/dist/lib/llm/types.js +2 -2
  83. package/dist/lib/llm/types.js.map +1 -1
  84. package/dist/lib/parsing/argument-parser.js +1 -1
  85. package/dist/lib/parsing/argument-parser.js.map +1 -1
  86. package/dist/lib/pipelines/execute.js +1 -1
  87. package/dist/lib/pipelines/execute.js.map +1 -1
  88. package/dist/lib/pipelines/stage-helpers.d.ts +1 -1
  89. package/dist/lib/pipelines/stage-helpers.d.ts.map +1 -1
  90. package/dist/lib/pipelines/stage-helpers.js +39 -2
  91. package/dist/lib/pipelines/stage-helpers.js.map +1 -1
  92. package/dist/lib/pipelines/types.d.ts +25 -0
  93. package/dist/lib/pipelines/types.d.ts.map +1 -1
  94. package/dist/lib/types/fork.d.ts +1 -1
  95. package/dist/lib/types/validation.js +9 -9
  96. package/dist/lib/types/validation.js.map +1 -1
  97. package/package.json +1 -1
@@ -38,6 +38,25 @@ export type TCreateOpenAiResponsesProviderOptions = {
38
38
  * `NonRetryableLlmError`.
39
39
  */
40
40
  backgroundMode?: boolean;
41
+ /**
42
+ * Combine OpenAI **background mode** with **live SSE streaming**:
43
+ * the request is submitted with `{ background: true, stream: true,
44
+ * store: true }` so it keeps generating server-side even if the
45
+ * connection drops, and the SSE stream is consumed live while it
46
+ * runs. This is independent of the `stream` option (foreground-
47
+ * only streaming) and `backgroundMode` (poll-only).
48
+ *
49
+ * When `true`, `backgroundMode` is ignored for this provider
50
+ * instance. `stream` is also ignored (SSE is always used in this
51
+ * mode). V1 supports the **no-tools path only**: a request that
52
+ * sets `backgroundStreamMode` and carries `tools` throws
53
+ * `NonRetryableLlmError`.
54
+ *
55
+ * The response id is surfaced immediately from the submit POST
56
+ * body (before any SSE bytes), making it available for durable
57
+ * persistence by the caller. Defaults to **`false`**.
58
+ */
59
+ backgroundStreamMode?: boolean;
41
60
  /**
42
61
  * Poll interval (ms) for the background submit-then-poll loop.
43
62
  * Defaults to 2000. Ignored unless `backgroundMode` is `true`.
@@ -45,4 +64,103 @@ export type TCreateOpenAiResponsesProviderOptions = {
45
64
  backgroundPollIntervalMs?: number;
46
65
  };
47
66
  export declare function createOpenAiResponsesProvider(options: TCreateOpenAiResponsesProviderOptions): TLlmProvider;
67
+ /**
68
+ * The status values the OpenAI Responses API reports for a stored
69
+ * response. `completed` / `failed` / `incomplete` / `cancelled` are
70
+ * terminal; `queued` / `in_progress` are transient.
71
+ */
72
+ export type TResponseStatus = "queued" | "in_progress" | "completed" | "failed" | "incomplete" | "cancelled";
73
+ /**
74
+ * The structured result of a `retrieveResponse` call. All fields
75
+ * except `status` and `rawResponseId` are absent for non-terminal
76
+ * or failed responses.
77
+ */
78
+ export type TRetrievedResponse = {
79
+ /** Current status of the stored response. */
80
+ status: TResponseStatus;
81
+ /**
82
+ * Parsed text output, present when `status === "completed"` and
83
+ * the response carried a `message` output item.
84
+ */
85
+ output?: string;
86
+ /** Token usage reported by OpenAI, when available. */
87
+ tokenUsage?: import("../../lib/llm/types.js").TLlmTokenUsage;
88
+ /** The OpenAI response id that was retrieved. */
89
+ rawResponseId: string;
90
+ };
91
+ /**
92
+ * Retrieve a stored OpenAI response by id. Surfaces the current
93
+ * status, output text (when completed), and token usage.
94
+ *
95
+ * Throws {@link ResponseNotFoundError} when the response is not found
96
+ * (HTTP 404), which typically means the ~10-minute retention window
97
+ * has elapsed. Callers should clear the stored id, settle the
98
+ * associated stage as failed, and surface a retry prompt.
99
+ *
100
+ * @param id - The OpenAI response id to retrieve.
101
+ * @param options - Provider configuration (apiKey, optional baseUrl and fetch).
102
+ */
103
+ export declare function retrieveResponse(id: string, options: {
104
+ apiKey: string;
105
+ baseUrl?: string;
106
+ fetch?: TOpenAiFetch;
107
+ signal?: AbortSignal;
108
+ }): Promise<TRetrievedResponse>;
109
+ /**
110
+ * Reconnect to a stored, still-generating background response and
111
+ * **stream it to completion**. This is what actually drives a dropped
112
+ * background response forward: a passive `retrieveResponse` GET only
113
+ * reads the current state and leaves a `queued` / `in_progress`
114
+ * response sitting where it is, whereas reconnecting with `stream=true`
115
+ * resumes consumption so the response reaches a terminal status.
116
+ *
117
+ * Issues `GET /responses/{id}?stream=true&starting_after=<cursor>` and
118
+ * consumes the SSE stream to its terminal event, returning the same
119
+ * {@link TRetrievedResponse} shape as {@link retrieveResponse}.
120
+ *
121
+ * Throws {@link ResponseNotFoundError} when the response is not found
122
+ * (HTTP 404 — typically the ~10-minute retention window elapsed).
123
+ * Honors `signal`: an abort propagates as an `AbortError` from the
124
+ * underlying stream read.
125
+ *
126
+ * @param id - The OpenAI response id to reconnect to.
127
+ * @param options - `apiKey`, optional `startingAfter` SSE cursor
128
+ * (defaults to 0 — replay from the start of the stored stream),
129
+ * optional `baseUrl`, `fetch`, and `signal`.
130
+ */
131
+ export declare function reconnectStream(id: string, options: {
132
+ apiKey: string;
133
+ startingAfter?: number;
134
+ baseUrl?: string;
135
+ fetch?: TOpenAiFetch;
136
+ signal?: AbortSignal;
137
+ }): Promise<TRetrievedResponse>;
138
+ /**
139
+ * Cancel a stored, in-flight OpenAI response. Issues
140
+ * `POST /responses/{id}/cancel` and returns the resulting
141
+ * {@link TRetrievedResponse} (typically `status: "cancelled"`).
142
+ *
143
+ * Cancel is **idempotent** per the Responses API: cancelling twice, or
144
+ * cancelling an already-terminal response, simply returns the final
145
+ * `Response` object rather than erroring — so callers do not need to
146
+ * guard against double-cancel.
147
+ *
148
+ * Throws {@link ResponseNotFoundError} when the response is not found
149
+ * (HTTP 404 — typically the ~10-minute retention window elapsed).
150
+ * Honors `signal` (an abort propagates as an `AbortError`).
151
+ *
152
+ * Use this to stop an in-flight background response when a stage is
153
+ * abandoned (resync timeout) or an import is cancelled, so generation
154
+ * does not keep running (and billing) server-side after the consumer
155
+ * has given up on it.
156
+ *
157
+ * @param id - The OpenAI response id to cancel.
158
+ * @param options - `apiKey`, optional `baseUrl`, `fetch`, and `signal`.
159
+ */
160
+ export declare function cancelResponse(id: string, options: {
161
+ apiKey: string;
162
+ baseUrl?: string;
163
+ fetch?: TOpenAiFetch;
164
+ signal?: AbortSignal;
165
+ }): Promise<TRetrievedResponse>;
48
166
  //# sourceMappingURL=provider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/extensions/openai/provider.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EACR,YAAY,EAKf,MAAM,wBAAwB,CAAA;AAe/B,OAAO,KAAK,EACR,YAAY,EAMf,MAAM,YAAY,CAAA;AAOnB,MAAM,MAAM,qCAAqC,GAAG;IAChD,MAAM,EAAE,MAAM,CAAA;IACd,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;OAGG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAA;CACpC,CAAA;AAED,wBAAgB,6BAA6B,CACzC,OAAO,EAAE,qCAAqC,GAC/C,YAAY,CAwSd"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/extensions/openai/provider.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EACR,YAAY,EAKf,MAAM,wBAAwB,CAAA;AAgB/B,OAAO,KAAK,EACR,YAAY,EAMf,MAAM,YAAY,CAAA;AAOnB,MAAM,MAAM,qCAAqC,GAAG;IAChD,MAAM,EAAE,MAAM,CAAA;IACd,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;;;;;;;;;;;;;OAiBG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B;;;OAGG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAA;CACpC,CAAA;AAED,wBAAgB,6BAA6B,CACzC,OAAO,EAAE,qCAAqC,GAC/C,YAAY,CA8Td;AAID;;;;GAIG;AACH,MAAM,MAAM,eAAe,GACrB,QAAQ,GACR,aAAa,GACb,WAAW,GACX,QAAQ,GACR,YAAY,GACZ,WAAW,CAAA;AAEjB;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC7B,6CAA6C;IAC7C,MAAM,EAAE,eAAe,CAAA;IACvB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,sDAAsD;IACtD,UAAU,CAAC,EAAE,OAAO,wBAAwB,EAAE,cAAc,CAAA;IAC5D,iDAAiD;IACjD,aAAa,EAAE,MAAM,CAAA;CACxB,CAAA;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CAClC,EAAE,EAAE,MAAM,EACV,OAAO,EAAE;IACL,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB,MAAM,CAAC,EAAE,WAAW,CAAA;CACvB,GACF,OAAO,CAAC,kBAAkB,CAAC,CAU7B;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,eAAe,CACjC,EAAE,EAAE,MAAM,EACV,OAAO,EAAE;IACL,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB,MAAM,CAAC,EAAE,WAAW,CAAA;CACvB,GACF,OAAO,CAAC,kBAAkB,CAAC,CAmC7B;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,cAAc,CAChC,EAAE,EAAE,MAAM,EACV,OAAO,EAAE;IACL,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB,MAAM,CAAC,EAAE,WAAW,CAAA;CACvB,GACF,OAAO,CAAC,kBAAkB,CAAC,CAoC7B"}
@@ -1,7 +1,7 @@
1
1
  // Concrete `TLlmProvider` backed by the OpenAI Responses API.
2
2
  //
3
- // Slice 1B (per the agenda + spec §6) provides the V1 adapter: raw
4
- // `fetch` to `https://api.openai.com/v1/responses` with strict-mode
3
+ // The V1 adapter: raw `fetch` to
4
+ // `https://api.openai.com/v1/responses` with strict-mode
5
5
  // structured output via the inlined TypeBox → JSON Schema converter,
6
6
  // translation of the framework's `TToolSpec` discriminated union
7
7
  // into the Responses-API tool-shape, and a function-tool agent loop
@@ -11,7 +11,7 @@
11
11
  //
12
12
  // The provider deliberately uses raw `fetch` rather than the
13
13
  // `openai` npm SDK. `openai` is declared as an optional peer in
14
- // `package.json` for forward-looking insurance — future slices may
14
+ // `package.json` for forward-looking insurance — a future version may
15
15
  // adopt it; V1 keeps the dependency surface minimal.
16
16
  //
17
17
  // Error classification routes HTTP-status families into framework-
@@ -30,7 +30,7 @@
30
30
  // readability.
31
31
  import { debugLlmFailure, debugLlmRequest, debugLlmResponse, } from "../../lib/pipelines/debug-log.js";
32
32
  import { typeboxToOpenAiSchema } from "./structured-output.js";
33
- import { NonRetryableLlmError, QuotaExhaustedLlmError, RateLimitLlmError, SchemaValidationLlmError, ToolLoopExhaustedError, TransientLlmError, } from "./errors.js";
33
+ import { NonRetryableLlmError, QuotaExhaustedLlmError, RateLimitLlmError, ResponseNotFoundError, SchemaValidationLlmError, ToolLoopExhaustedError, TransientLlmError, } from "./errors.js";
34
34
  const STAGE_ID_MARKER = /<!--\s*stage-id:\s*([^\s>]+)\s*-->/;
35
35
  const DEFAULT_BASE_URL = "https://api.openai.com/v1/responses";
36
36
  const DEFAULT_MAX_TOOL_ROUNDS = 6;
@@ -42,12 +42,17 @@ export function createOpenAiResponsesProvider(options) {
42
42
  }
43
43
  const maxToolRounds = options.maxToolCallRounds ?? DEFAULT_MAX_TOOL_ROUNDS;
44
44
  const useStream = options.stream ?? true;
45
- const useBackground = options.backgroundMode ?? false;
45
+ const useBackgroundStream = options.backgroundStreamMode ?? false;
46
+ const useBackground = useBackgroundStream
47
+ ? false
48
+ : (options.backgroundMode ?? false);
46
49
  const backgroundPollIntervalMs = options.backgroundPollIntervalMs ?? 2000;
47
50
  const respond = async (req) => {
48
- if (useBackground && req.tools && req.tools.length > 0) {
51
+ if ((useBackground || useBackgroundStream) &&
52
+ req.tools &&
53
+ req.tools.length > 0) {
49
54
  throw new NonRetryableLlmError({
50
- message: "OpenAI background mode does not support function tools in V1. Disable backgroundMode for tool-using requests, or run the tools synchronously.",
55
+ message: "OpenAI background mode does not support function tools in V1. Disable backgroundMode / backgroundStreamMode for tool-using requests, or run the tools synchronously.",
51
56
  });
52
57
  }
53
58
  const schemaName = deriveSchemaName(req.outputSchema);
@@ -68,6 +73,17 @@ export function createOpenAiResponsesProvider(options) {
68
73
  ];
69
74
  let lastUsage = { input: 0, output: 0 };
70
75
  let lastResponseId;
76
+ // Fire the mid-flight id callback at most once across the whole
77
+ // call (background-stream mode is no-tools-only, so the loop runs
78
+ // a single round, but guard regardless of future loop behavior).
79
+ let responseIdNotified = false;
80
+ const notifyResponseId = (responseId) => {
81
+ lastResponseId = responseId;
82
+ if (responseIdNotified)
83
+ return;
84
+ responseIdNotified = true;
85
+ req.onResponseCreated?.(responseId);
86
+ };
71
87
  for (let round = 0; round < maxToolRounds; round += 1) {
72
88
  const body = {
73
89
  model: req.model,
@@ -108,8 +124,13 @@ export function createOpenAiResponsesProvider(options) {
108
124
  signal: req.signal,
109
125
  stream: useStream,
110
126
  background: useBackground,
127
+ backgroundStream: useBackgroundStream,
111
128
  pollIntervalMs: backgroundPollIntervalMs,
129
+ onResponseId: notifyResponseId,
112
130
  });
131
+ // The mid-flight callback (background-stream mode) already set
132
+ // `lastResponseId`; fall back to the terminal envelope id for
133
+ // the synchronous / poll paths where no mid-flight id fires.
113
134
  lastResponseId = envelope.id ?? lastResponseId;
114
135
  lastUsage = mergeUsage(lastUsage, extractUsage(envelope));
115
136
  if (envelope.status === "failed") {
@@ -220,8 +241,7 @@ export function createOpenAiResponsesProvider(options) {
220
241
  // `function_call_output`. Omitting the
221
242
  // `function_call` items returns a 400 with a
222
243
  // conversation-state error on round 2+. Order is
223
- // preserved across all calls in the round (slice
224
- // 1B.1 reviewer fold P1 #1).
244
+ // preserved across all calls in the round.
225
245
  for (const call of functionCalls) {
226
246
  const handler = findFunctionHandler(req.tools, call.name);
227
247
  if (!handler) {
@@ -301,8 +321,165 @@ export function createOpenAiResponsesProvider(options) {
301
321
  };
302
322
  return { respond };
303
323
  }
324
+ /**
325
+ * Retrieve a stored OpenAI response by id. Surfaces the current
326
+ * status, output text (when completed), and token usage.
327
+ *
328
+ * Throws {@link ResponseNotFoundError} when the response is not found
329
+ * (HTTP 404), which typically means the ~10-minute retention window
330
+ * has elapsed. Callers should clear the stored id, settle the
331
+ * associated stage as failed, and surface a retry prompt.
332
+ *
333
+ * @param id - The OpenAI response id to retrieve.
334
+ * @param options - Provider configuration (apiKey, optional baseUrl and fetch).
335
+ */
336
+ export async function retrieveResponse(id, options) {
337
+ const fetchImpl = resolveFetch(options.fetch, "retrieveResponse");
338
+ const envelope = await getResponseById({
339
+ url: options.baseUrl ?? DEFAULT_BASE_URL,
340
+ id,
341
+ apiKey: options.apiKey,
342
+ fetchImpl,
343
+ signal: options.signal,
344
+ });
345
+ return envelopeToRetrievedResponse(envelope, id);
346
+ }
347
+ /**
348
+ * Reconnect to a stored, still-generating background response and
349
+ * **stream it to completion**. This is what actually drives a dropped
350
+ * background response forward: a passive `retrieveResponse` GET only
351
+ * reads the current state and leaves a `queued` / `in_progress`
352
+ * response sitting where it is, whereas reconnecting with `stream=true`
353
+ * resumes consumption so the response reaches a terminal status.
354
+ *
355
+ * Issues `GET /responses/{id}?stream=true&starting_after=<cursor>` and
356
+ * consumes the SSE stream to its terminal event, returning the same
357
+ * {@link TRetrievedResponse} shape as {@link retrieveResponse}.
358
+ *
359
+ * Throws {@link ResponseNotFoundError} when the response is not found
360
+ * (HTTP 404 — typically the ~10-minute retention window elapsed).
361
+ * Honors `signal`: an abort propagates as an `AbortError` from the
362
+ * underlying stream read.
363
+ *
364
+ * @param id - The OpenAI response id to reconnect to.
365
+ * @param options - `apiKey`, optional `startingAfter` SSE cursor
366
+ * (defaults to 0 — replay from the start of the stored stream),
367
+ * optional `baseUrl`, `fetch`, and `signal`.
368
+ */
369
+ export async function reconnectStream(id, options) {
370
+ const fetchImpl = resolveFetch(options.fetch, "reconnectStream");
371
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
372
+ const startingAfter = options.startingAfter ?? 0;
373
+ const url = `${baseUrl}/${id}?stream=true&starting_after=${startingAfter.toString()}`;
374
+ let response;
375
+ try {
376
+ response = await fetchImpl(url, {
377
+ method: "GET",
378
+ headers: { Authorization: `Bearer ${options.apiKey}` },
379
+ signal: options.signal,
380
+ });
381
+ }
382
+ catch (err) {
383
+ if (isAbortError(err))
384
+ throw err;
385
+ throw new TransientLlmError({
386
+ message: `Network error reconnecting to OpenAI background response: ${err instanceof Error ? err.message : String(err)}`,
387
+ });
388
+ }
389
+ if (response.status === 404) {
390
+ throw new ResponseNotFoundError({ responseId: id });
391
+ }
392
+ if (!response.ok) {
393
+ const errorBody = await response.text().catch(() => "");
394
+ throw classifyHttpError(response.status, `OpenAI reconnect ${response.status.toString()}: ${errorBody || response.statusText}`);
395
+ }
396
+ const envelope = await readSseEnvelope(response);
397
+ return envelopeToRetrievedResponse(envelope, id);
398
+ }
399
+ /**
400
+ * Cancel a stored, in-flight OpenAI response. Issues
401
+ * `POST /responses/{id}/cancel` and returns the resulting
402
+ * {@link TRetrievedResponse} (typically `status: "cancelled"`).
403
+ *
404
+ * Cancel is **idempotent** per the Responses API: cancelling twice, or
405
+ * cancelling an already-terminal response, simply returns the final
406
+ * `Response` object rather than erroring — so callers do not need to
407
+ * guard against double-cancel.
408
+ *
409
+ * Throws {@link ResponseNotFoundError} when the response is not found
410
+ * (HTTP 404 — typically the ~10-minute retention window elapsed).
411
+ * Honors `signal` (an abort propagates as an `AbortError`).
412
+ *
413
+ * Use this to stop an in-flight background response when a stage is
414
+ * abandoned (resync timeout) or an import is cancelled, so generation
415
+ * does not keep running (and billing) server-side after the consumer
416
+ * has given up on it.
417
+ *
418
+ * @param id - The OpenAI response id to cancel.
419
+ * @param options - `apiKey`, optional `baseUrl`, `fetch`, and `signal`.
420
+ */
421
+ export async function cancelResponse(id, options) {
422
+ const fetchImpl = resolveFetch(options.fetch, "cancelResponse");
423
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
424
+ let response;
425
+ try {
426
+ response = await fetchImpl(`${baseUrl}/${id}/cancel`, {
427
+ method: "POST",
428
+ headers: { Authorization: `Bearer ${options.apiKey}` },
429
+ signal: options.signal,
430
+ });
431
+ }
432
+ catch (err) {
433
+ if (isAbortError(err))
434
+ throw err;
435
+ throw new TransientLlmError({
436
+ message: `Network error cancelling OpenAI background response: ${err instanceof Error ? err.message : String(err)}`,
437
+ });
438
+ }
439
+ if (response.status === 404) {
440
+ throw new ResponseNotFoundError({ responseId: id });
441
+ }
442
+ if (!response.ok) {
443
+ const errorBody = await response.text().catch(() => "");
444
+ throw classifyHttpError(response.status, `OpenAI cancel ${response.status.toString()}: ${errorBody || response.statusText}`);
445
+ }
446
+ const envelope = await parseJsonOrThrowTransient(response, "OpenAI cancel body was not valid JSON");
447
+ return envelopeToRetrievedResponse(envelope, id);
448
+ }
449
+ function resolveFetch(injected, fnName) {
450
+ const fetchImpl = injected ?? globalThis.fetch;
451
+ if (!fetchImpl) {
452
+ throw new Error(`${fnName}: no fetch implementation available. Pass \`fetch\` explicitly or run in an environment that provides \`globalThis.fetch\` (Node ≥18, modern browsers, Expo).`);
453
+ }
454
+ return fetchImpl;
455
+ }
456
+ function envelopeToRetrievedResponse(envelope, id) {
457
+ const output = extractAssistantText(envelope.output);
458
+ const usage = extractUsage(envelope);
459
+ const result = {
460
+ status: (envelope.status ?? "in_progress"),
461
+ rawResponseId: envelope.id ?? id,
462
+ };
463
+ if (output !== undefined) {
464
+ result.output = output;
465
+ }
466
+ if (usage.input > 0 || usage.output > 0) {
467
+ result.tokenUsage = usage;
468
+ }
469
+ return result;
470
+ }
304
471
  // -- HTTP --
305
472
  async function fetchResponseEnvelope(args) {
473
+ if (args.backgroundStream) {
474
+ return runBackgroundStream({
475
+ url: args.url,
476
+ apiKey: args.apiKey,
477
+ body: args.body,
478
+ fetchImpl: args.fetchImpl,
479
+ signal: args.signal,
480
+ onResponseId: args.onResponseId,
481
+ });
482
+ }
306
483
  if (args.background) {
307
484
  return runBackground({
308
485
  url: args.url,
@@ -389,6 +566,12 @@ async function getResponseById(args) {
389
566
  message: `Network error polling OpenAI background response: ${err instanceof Error ? err.message : String(err)}`,
390
567
  });
391
568
  }
569
+ if (response.status === 404) {
570
+ // Response has aged out of the ~10-minute retention window or
571
+ // was never stored. Surface as a typed error so callers can
572
+ // clear the stored id and settle the stage as failed.
573
+ throw new ResponseNotFoundError({ responseId: args.id });
574
+ }
392
575
  if (!response.ok) {
393
576
  const errorBody = await response.text().catch(() => "");
394
577
  throw classifyHttpError(response.status, `OpenAI poll ${response.status.toString()}: ${errorBody || response.statusText}`);
@@ -417,6 +600,40 @@ function isTerminalBackgroundStatus(status) {
417
600
  status === "incomplete" ||
418
601
  status === "cancelled");
419
602
  }
603
+ /**
604
+ * Submit a single request with `{ background: true, stream: true,
605
+ * store: true }` and consume the resulting SSE stream live, returning
606
+ * the terminal envelope.
607
+ *
608
+ * A background response can only be streamed if it was *created* with
609
+ * `stream: true` (a background-without-stream response is poll-only and
610
+ * cannot later be streamed), so this mode uses one streaming create
611
+ * call rather than a separate non-streaming submit POST. The response
612
+ * id is therefore not in a JSON POST body — it arrives in the first
613
+ * `response.created` SSE event. `onResponseId` fires the moment that
614
+ * event is parsed (before the terminal event), so the caller can
615
+ * persist the id while the call is still in flight.
616
+ *
617
+ * The response keeps generating server-side even if the connection
618
+ * drops during stream consumption, and can be recovered via
619
+ * `retrieveResponse` within the ~10-minute retention window. A
620
+ * connection drop mid-stream (no terminal event before stream end) is
621
+ * classified as a `TransientLlmError` so the framework's retry policy
622
+ * applies — but because the id was already surfaced mid-flight, a
623
+ * crashed in-flight call can be recovered rather than blindly re-run.
624
+ */
625
+ async function runBackgroundStream(args) {
626
+ if (args.signal?.aborted)
627
+ throw abortError();
628
+ const httpResponse = await callOnce({
629
+ url: args.url,
630
+ apiKey: args.apiKey,
631
+ body: { ...args.body, background: true, stream: true, store: true },
632
+ fetchImpl: args.fetchImpl,
633
+ signal: args.signal,
634
+ });
635
+ return readSseEnvelope(httpResponse, args.onResponseId);
636
+ }
420
637
  async function runBackground(args) {
421
638
  if (args.signal?.aborted)
422
639
  throw abortError();
@@ -555,7 +772,7 @@ function classifyHttpError(status, message, providerErrorCode) {
555
772
  }
556
773
  return new RateLimitLlmError({ message, status });
557
774
  }
558
- // 400 vs 422 split (slice 1B.1 reviewer fold P2 #1):
775
+ // 400 vs 422 split:
559
776
  //
560
777
  // OpenAI returns 400 for malformed requests — typically a
561
778
  // converter bug, an unsupported parameter, or a request shape
@@ -603,12 +820,23 @@ const SSE_TERMINAL_EVENTS = new Set([
603
820
  "response.incomplete",
604
821
  "response.failed",
605
822
  ]);
823
+ // The lifecycle event the Responses API emits first, before any output
824
+ // chunks. It carries the response object — including `.id` — so a
825
+ // streaming consumer learns the id while the call is still in flight.
826
+ const SSE_CREATED_EVENT = "response.created";
606
827
  /**
607
- * Parse one SSE event block. Returns the embedded full `response`
608
- * envelope when the event is a terminal Responses-API event
609
- * (`response.completed` / `.incomplete` / `.failed`); otherwise
610
- * `undefined`. The terminal events carry a `type` field inside the
611
- * data JSON, so we key off that and ignore the `event:` line.
828
+ * Parse one SSE event block. Returns:
829
+ *
830
+ * * `{ kind: "terminal", envelope }` for a terminal Responses-API
831
+ * event (`response.completed` / `.incomplete` / `.failed`),
832
+ * carrying the embedded full `response` envelope;
833
+ * * `{ kind: "created", responseId }` for the lifecycle
834
+ * `response.created` event, surfacing the response id the moment
835
+ * it is known (before any output);
836
+ * * `undefined` for every intermediate / unrecognized event.
837
+ *
838
+ * The events carry a `type` field inside the data JSON, so we key off
839
+ * that and fall back to the SSE `event:` line.
612
840
  */
613
841
  function parseSseEvent(raw) {
614
842
  let eventType;
@@ -639,7 +867,10 @@ function parseSseEvent(raw) {
639
867
  // the Responses API does not emit.)
640
868
  const type = parsed.type ?? eventType;
641
869
  if (type && SSE_TERMINAL_EVENTS.has(type) && parsed.response) {
642
- return parsed.response;
870
+ return { kind: "terminal", envelope: parsed.response };
871
+ }
872
+ if (type === SSE_CREATED_EVENT && parsed.response?.id) {
873
+ return { kind: "created", responseId: parsed.response.id };
643
874
  }
644
875
  return undefined;
645
876
  }
@@ -650,11 +881,18 @@ function parseSseEvent(raw) {
650
881
  * `AbortError` from the underlying reader propagates verbatim so
651
882
  * `llmStage` marks the stage `skipped`.
652
883
  *
884
+ * `onResponseId`, when supplied, fires the moment the `response.created`
885
+ * lifecycle event is parsed — i.e. while the call is still streaming,
886
+ * before the terminal event arrives. This is the load-bearing seam for
887
+ * background-stream mode: it lets a caller persist the response id
888
+ * mid-flight so an in-flight call interrupted before completion can be
889
+ * recovered from the upstream's stored copy. Invoked at most once.
890
+ *
653
891
  * Note: the event-separator scan assumes LF (`\n\n`) framing, which the
654
892
  * OpenAI Responses API emits. Reuse against a strict CRLF-only SSE
655
893
  * server would need the separator scan adjusted to `\r\n\r\n`.
656
894
  */
657
- async function readSseEnvelope(response) {
895
+ async function readSseEnvelope(response, onResponseId) {
658
896
  const body = response.body;
659
897
  if (!body) {
660
898
  throw new TransientLlmError({
@@ -665,6 +903,21 @@ async function readSseEnvelope(response) {
665
903
  const decoder = new TextDecoder();
666
904
  let buffer = "";
667
905
  let terminal;
906
+ let idSurfaced = false;
907
+ const handleEvent = (rawEvent) => {
908
+ const parsedEvent = parseSseEvent(rawEvent);
909
+ if (!parsedEvent)
910
+ return;
911
+ if (parsedEvent.kind === "terminal") {
912
+ terminal = parsedEvent.envelope;
913
+ return;
914
+ }
915
+ // kind === "created": surface the id once, the moment it's known.
916
+ if (!idSurfaced) {
917
+ idSurfaced = true;
918
+ onResponseId?.(parsedEvent.responseId);
919
+ }
920
+ };
668
921
  try {
669
922
  for (;;) {
670
923
  const chunk = await reader.read();
@@ -675,9 +928,7 @@ async function readSseEnvelope(response) {
675
928
  while (sep !== -1) {
676
929
  const rawEvent = buffer.slice(0, sep);
677
930
  buffer = buffer.slice(sep + 2);
678
- const env = parseSseEvent(rawEvent);
679
- if (env)
680
- terminal = env;
931
+ handleEvent(rawEvent);
681
932
  sep = buffer.indexOf("\n\n");
682
933
  }
683
934
  }
@@ -698,9 +949,7 @@ async function readSseEnvelope(response) {
698
949
  while (tailSep !== -1) {
699
950
  const rawEvent = buffer.slice(0, tailSep);
700
951
  buffer = buffer.slice(tailSep + 2);
701
- const env = parseSseEvent(rawEvent);
702
- if (env)
703
- terminal = env;
952
+ handleEvent(rawEvent);
704
953
  tailSep = buffer.indexOf("\n\n");
705
954
  }
706
955
  if (!terminal) {