@agentionai/agents 1.14.0 → 1.15.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.
@@ -144,10 +144,31 @@ export interface OpenAISpecificConfig {
144
144
  */
145
145
  promptCacheKey?: string;
146
146
  /**
147
- * How long cached prefixes stay warm — `"24h"` opts into extended retention.
147
+ * How long cached prefixes stay warm — `"in_memory"` expires them within
148
+ * minutes, `"24h"` keeps them up to a day. Left unset the default follows
149
+ * your organization's data-retention policy (`"24h"` without ZDR,
150
+ * `"in_memory"` with it), so set it explicitly if you care either way.
148
151
  * Ignored by the ChatGPT/Codex backend, which manages its own cache.
149
152
  */
150
- promptCacheRetention?: "in-memory" | "24h";
153
+ promptCacheRetention?: "in_memory" | "24h";
154
+ /**
155
+ * Request each turn's reasoning as an encrypted blob and replay it on every
156
+ * later request of the conversation, so a reasoning model keeps its own
157
+ * thinking across tool hops and turns instead of re-deriving it.
158
+ *
159
+ * Setting it to `false` stops both halves — nothing is requested, and blobs
160
+ * already in the history are not sent either, which is what makes it a way
161
+ * out of the model-switch rejection below.
162
+ *
163
+ * Defaults to on for models known to reason on OpenAI's own API, off
164
+ * otherwise — including behind a custom `baseURL`, where the host may not
165
+ * support the parameter. `CodexAgent` defaults it to on for every model.
166
+ *
167
+ * Reasoning blobs are tied to the model that produced them: switching models
168
+ * mid-conversation with a history full of them is rejected. Clear the history
169
+ * or turn this off when doing that.
170
+ */
171
+ includeEncryptedReasoning?: boolean;
151
172
  /**
152
173
  * Override the API base URL. Defaults to `api.openai.com/v1`; `CodexAgent`
153
174
  * defaults it to `https://chatgpt.com/backend-api/codex`, and setting it
@@ -159,6 +180,14 @@ export interface OpenAISpecificConfig {
159
180
  * `chatgpt-account-id` header.
160
181
  */
161
182
  accountId?: string;
183
+ /**
184
+ * `CodexAgent` only: conversation id sent as the `session_id` header, which
185
+ * is what routes requests to a shared prompt cache on that backend — setting
186
+ * it is how you opt into caching there. Unset by default, so the header is
187
+ * omitted and the request stays byte-identical; `randomUUID()` per agent
188
+ * gives a per-run cache, a stable id shares one across runs.
189
+ */
190
+ sessionId?: string;
162
191
  /**
163
192
  * `CodexAgent` only: client identifier sent as the `originator` header.
164
193
  * OpenAI varies the model catalog by originator.
@@ -49,7 +49,57 @@ export type CodexAgentConfig = Omit<OpenAiAgentConfig, "model" | "reasoningEffor
49
49
  * @default CODEX_CLIENT_VERSION
50
50
  */
51
51
  clientVersion?: string;
52
+ /**
53
+ * Conversation identifier sent as the `session_id` header, as the Codex CLI
54
+ * does — **this is what makes prompt caching work on this backend**, and
55
+ * setting it is how you opt into that caching.
56
+ *
57
+ * Requests carrying the same value are routed together and reuse each other's
58
+ * cached prefix; without it the backend caches essentially nothing, however
59
+ * identical the prefix. Measured on 2026-09-10 with a ~9K-token prefix
60
+ * repeated 8 times: 0/14 calls hit the cache with no header, 12/14 hit
61
+ * (~98% of the prefix) with one. `prompt_cache_key`, the platform API's
62
+ * lever, makes no difference here.
63
+ *
64
+ * **Unset by default**, so the header is omitted entirely and a request stays
65
+ * byte-identical to what earlier versions sent — the same opt-in rule as
66
+ * {@link AgentConfig.promptCacheKey} and
67
+ * {@link AgentConfig.promptCacheRetention}. Caching is not free of side
68
+ * effects: it groups your requests server-side under an id you chose, so it
69
+ * is yours to turn on rather than the agent's to assume.
70
+ *
71
+ * Any stable string works; the value is opaque and only its stability
72
+ * matters. One id per conversation is the usual grain — `randomUUID()` per
73
+ * agent instance reproduces the old default:
74
+ *
75
+ * ```typescript
76
+ * new CodexAgent({ …, sessionId: randomUUID() }) // cache within this run
77
+ * new CodexAgent({ …, sessionId: conversationId }) // cache across runs
78
+ * ```
79
+ *
80
+ * Note it is a *header*: `session_id` in the request body is rejected with
81
+ * *"Unsupported parameter: session_id"*.
82
+ */
83
+ sessionId?: string;
84
+ };
85
+ /**
86
+ * Configuration a factory hands to the constructor, before the credentials it
87
+ * resolves are merged in.
88
+ */
89
+ type CodexFactoryConfig = Omit<CodexAgentConfig, "apiKey" | "accountId"> & {
90
+ tokenOptions?: CodexTokenProviderOptions;
52
91
  };
92
+ /**
93
+ * What a static factory needs of the class it was called on: a constructor for
94
+ * the concrete subclass — which is where `T` is inferred from — intersected
95
+ * with the sibling factory it delegates to, so an override of that runs too.
96
+ *
97
+ * `Pick<typeof CodexAgent, …>` rather than a hand-written signature: the
98
+ * sibling's own `this` parameter is what carries `T` through the delegation,
99
+ * and restating it here would erase that and pin every subclass back to the
100
+ * base type.
101
+ */
102
+ type CodexAgentClass<T extends CodexAgent> = (new (config: CodexAgentConfig, history?: History) => T) & Pick<typeof CodexAgent, "fromCredentials">;
53
103
  /**
54
104
  * Agent for OpenAI models reached through a **ChatGPT subscription** rather
55
105
  * than a platform API key.
@@ -88,6 +138,15 @@ export declare class CodexAgent extends OpenAiAgent<OpenAIModel, CodexModelCard>
88
138
  private readonly originator;
89
139
  private readonly clientVersion;
90
140
  private readonly codexBaseURL;
141
+ /**
142
+ * The `session_id` this agent sends on every request, or `undefined` when the
143
+ * header is not being sent — the key the backend's prompt cache is routed by.
144
+ *
145
+ * `undefined` means prompt caching is effectively off for this agent; set
146
+ * {@link CodexSpecificConfig.sessionId} to opt in. Read it back to pin a
147
+ * later agent to the same cache.
148
+ */
149
+ readonly sessionId?: string;
91
150
  /**
92
151
  * Quota state written by the fetch wrapper installed in the constructor.
93
152
  *
@@ -126,18 +185,30 @@ export declare class CodexAgent extends OpenAiAgent<OpenAIModel, CodexModelCard>
126
185
  * Build an agent from the credentials `codex login` stored, wrapped in a
127
186
  * provider that refreshes the access token as it ages out.
128
187
  *
188
+ * Constructs `this`, so `MyCodexAgent.fromCodexCli(…)` returns a
189
+ * `MyCodexAgent` — see {@link CodexAgent.fromCredentials}.
190
+ *
129
191
  * @throws if no credentials are present — run `codex login` first.
130
192
  */
131
- static fromCodexCli(config: Omit<CodexAgentConfig, "apiKey" | "accountId"> & {
193
+ static fromCodexCli<T extends CodexAgent>(this: CodexAgentClass<T>, config: CodexFactoryConfig & {
132
194
  /** Read `auth.json` from somewhere other than `$CODEX_HOME`. */
133
195
  codexHome?: string;
134
- /** Forwarded to {@link createCodexTokenProvider}. */
135
- tokenOptions?: CodexTokenProviderOptions;
136
- }, history?: History): Promise<CodexAgent>;
137
- /** Build an agent from credentials obtained however you like. */
138
- static fromCredentials(credentials: CodexCredentials, config: Omit<CodexAgentConfig, "apiKey" | "accountId"> & {
139
- tokenOptions?: CodexTokenProviderOptions;
140
- }, history?: History): CodexAgent;
196
+ }, history?: History): Promise<T>;
197
+ /**
198
+ * Build an agent from credentials obtained however you like.
199
+ *
200
+ * Instantiates `this` rather than `CodexAgent`, so a subclass gets its own
201
+ * type back and its overrides actually run. Hard-coding the class here made
202
+ * `class MyCodexAgent extends CodexAgent` silently produce a plain
203
+ * `CodexAgent` — no error, no override, and nothing to see until an
204
+ * experiment came back saying the change under test had no effect.
205
+ */
206
+ static fromCredentials<T extends CodexAgent>(this: new (config: CodexAgentConfig, history?: History) => T, credentials: CodexCredentials, config: CodexFactoryConfig, history?: History): T;
207
+ /**
208
+ * Every model on this backend reasons, and this is what the Codex CLI itself
209
+ * does, so the encrypted-reasoning round trip is on unless turned off.
210
+ */
211
+ protected defaultIncludeEncryptedReasoning(): boolean;
141
212
  /** This backend refuses `stream: false` outright. */
142
213
  protected get forceStreaming(): boolean;
143
214
  /**
@@ -166,4 +237,5 @@ export declare class CodexAgent extends OpenAiAgent<OpenAIModel, CodexModelCard>
166
237
  */
167
238
  listModels(): Promise<ModelInfo<CodexModelCard>[]>;
168
239
  }
240
+ export {};
169
241
  //# sourceMappingURL=CodexAgent.d.ts.map
@@ -45,6 +45,9 @@ class CodexAgent extends OpenAiAgent_1.OpenAiAgent {
45
45
  const accountId = config.accountId ?? vendorConfig.accountId;
46
46
  const originator = config.originator ?? vendorConfig.originator ?? codex_auth_1.CODEX_ORIGINATOR;
47
47
  const baseURL = config.baseURL ?? vendorConfig.baseURL ?? codex_auth_1.CODEX_BASE_URL;
48
+ // No generated fallback: caching is opt-in, so an unset id means the header
49
+ // is not sent at all.
50
+ const sessionId = config.sessionId ?? vendorConfig.sessionId;
48
51
  // Filled by the fetch wrapper below and adopted as `this.limits` once
49
52
  // `super()` has run.
50
53
  const limits = {};
@@ -60,6 +63,11 @@ class CodexAgent extends OpenAiAgent_1.OpenAiAgent {
60
63
  ...(accountId ? { "chatgpt-account-id": accountId } : {}),
61
64
  "OpenAI-Beta": "responses=experimental",
62
65
  originator,
66
+ // Opt-in. Stable for the life of the agent: the backend keys its
67
+ // prompt cache on this, and drops to ~0% hit rate without it. Omitted
68
+ // entirely when unset, so the request is byte-identical to one sent
69
+ // before this existed.
70
+ ...(sessionId ? { session_id: sessionId } : {}),
63
71
  // Every Codex request is a stream; the SDK would send
64
72
  // `application/json`, which no reference client does.
65
73
  Accept: "text/event-stream",
@@ -86,8 +94,11 @@ class CodexAgent extends OpenAiAgent_1.OpenAiAgent {
86
94
  this.accountId = accountId;
87
95
  this.originator = originator;
88
96
  this.clientVersion =
89
- config.clientVersion ?? vendorConfig.clientVersion ?? codex_auth_1.CODEX_CLIENT_VERSION;
97
+ config.clientVersion ??
98
+ vendorConfig.clientVersion ??
99
+ codex_auth_1.CODEX_CLIENT_VERSION;
90
100
  this.codexBaseURL = baseURL;
101
+ this.sessionId = sessionId;
91
102
  }
92
103
  /**
93
104
  * What the most recent response said about the subscription's remaining
@@ -119,16 +130,29 @@ class CodexAgent extends OpenAiAgent_1.OpenAiAgent {
119
130
  * Build an agent from the credentials `codex login` stored, wrapped in a
120
131
  * provider that refreshes the access token as it ages out.
121
132
  *
133
+ * Constructs `this`, so `MyCodexAgent.fromCodexCli(…)` returns a
134
+ * `MyCodexAgent` — see {@link CodexAgent.fromCredentials}.
135
+ *
122
136
  * @throws if no credentials are present — run `codex login` first.
123
137
  */
124
138
  static async fromCodexCli(config, history) {
125
139
  const credentials = await (0, codex_auth_1.loadCodexCredentials)(config.codexHome);
126
- return CodexAgent.fromCredentials(credentials, config, history);
140
+ // `this`, not `CodexAgent`: routed through the subclass so an override of
141
+ // `fromCredentials` is not skipped either.
142
+ return this.fromCredentials(credentials, config, history);
127
143
  }
128
- /** Build an agent from credentials obtained however you like. */
144
+ /**
145
+ * Build an agent from credentials obtained however you like.
146
+ *
147
+ * Instantiates `this` rather than `CodexAgent`, so a subclass gets its own
148
+ * type back and its overrides actually run. Hard-coding the class here made
149
+ * `class MyCodexAgent extends CodexAgent` silently produce a plain
150
+ * `CodexAgent` — no error, no override, and nothing to see until an
151
+ * experiment came back saying the change under test had no effect.
152
+ */
129
153
  static fromCredentials(credentials, config, history) {
130
154
  const tokens = (0, codex_auth_1.createCodexTokenProvider)(credentials, config.tokenOptions);
131
- return new CodexAgent({
155
+ return new this({
132
156
  ...config,
133
157
  // The function form: the SDK re-invokes it before every request, so a
134
158
  // long run outlives the ~1h token.
@@ -136,6 +160,13 @@ class CodexAgent extends OpenAiAgent_1.OpenAiAgent {
136
160
  accountId: credentials.accountId,
137
161
  }, history);
138
162
  }
163
+ /**
164
+ * Every model on this backend reasons, and this is what the Codex CLI itself
165
+ * does, so the encrypted-reasoning round trip is on unless turned off.
166
+ */
167
+ defaultIncludeEncryptedReasoning() {
168
+ return true;
169
+ }
139
170
  /** This backend refuses `stream: false` outright. */
140
171
  get forceStreaming() {
141
172
  return true;
@@ -59,13 +59,56 @@ export type AgentConfig<M extends OpenAIModel = OpenAIModel> = Omit<BaseAgentCon
59
59
  */
60
60
  promptCacheKey?: string;
61
61
  /**
62
- * How long cached prefixes stay warm. `"24h"` opts into extended retention;
63
- * the default (`undefined`, i.e. the API's `in-memory`) expires a prefix
64
- * within minutes.
62
+ * Ask for each turn's reasoning to come back as an encrypted, replayable
63
+ * blob (`include: ["reasoning.encrypted_content"]`), and send those blobs
64
+ * back on every later request of the conversation.
65
+ *
66
+ * Both halves matter, and both happen here: requesting the blobs without
67
+ * replaying them changes nothing, and replaying is only possible because the
68
+ * agent runs with `store: false`, which leaves the provider holding no
69
+ * reasoning of its own.
70
+ *
71
+ * Why it is worth the bytes on a reasoning model: the model's own thinking is
72
+ * part of what it saw when it decided to call a tool, so dropping it between
73
+ * hops makes the model re-derive it — and, because the replayed prefix no
74
+ * longer matches what was processed last turn, breaks the prompt cache from
75
+ * the first turn onward.
76
+ *
77
+ * Setting it to `false` stops both halves too: nothing is requested, and
78
+ * blobs already sitting in the history are not replayed either. That is what
79
+ * makes turning it off a way out of the model-switch rejection below, rather
80
+ * than a half-measure that keeps sending the old model's reasoning.
81
+ *
82
+ * Defaults to on for models known to reason (see
83
+ * {@link OPENAI_REASONING_SUPPORT}) *on OpenAI's own API*, and off otherwise
84
+ * — including behind a custom `baseURL`, since a model name says nothing
85
+ * about whether the host serving it accepts the parameter. So a non-reasoning
86
+ * model such as `gpt-4.1-mini`, and any gateway or local server, sends a
87
+ * byte-identical request. Set it explicitly for a model too new to be in that
88
+ * table, or for a compatible host.
89
+ *
90
+ * Reasoning blobs are tied to the model that produced them: switching models
91
+ * mid-conversation with a history full of them is rejected. Clear the history
92
+ * or turn this off when doing that.
93
+ */
94
+ includeEncryptedReasoning?: boolean;
95
+ /**
96
+ * How long cached prefixes stay warm. `"in_memory"` expires a prefix after
97
+ * minutes of inactivity (an hour at the outside); `"24h"` keeps it up to a
98
+ * day.
99
+ *
100
+ * Left unset, the default is your organization's data-retention policy, not
101
+ * a fixed value: orgs *without* ZDR default to `"24h"`, orgs *with* ZDR to
102
+ * `"in_memory"`. Set it explicitly to stop prefixes being retained for a day
103
+ * without having to enable ZDR account-wide.
104
+ *
105
+ * `gpt-5.5` and later accept only `"24h"` here, and the field is deprecated
106
+ * upstream in favour of `prompt_cache_options.ttl` — the two are independent
107
+ * (this is a *maximum* retention policy, `ttl` a *minimum* lifetime).
65
108
  *
66
109
  * Ignored by the ChatGPT/Codex backend, which manages its own cache.
67
110
  */
68
- promptCacheRetention?: "in-memory" | "24h";
111
+ promptCacheRetention?: "in_memory" | "24h";
69
112
  };
70
113
  /**
71
114
  * Lowest `reasoning.effort` the given model accepts, used to resolve
@@ -86,16 +129,19 @@ export declare function lowestReasoningEffort(model: string | undefined): Reason
86
129
  /**
87
130
  * `usage.input_tokens_details` as the wire actually carries it.
88
131
  *
89
- * The SDK's `ResponseUsage.InputTokensDetails` declares `cached_tokens` alone,
90
- * but the ChatGPT/Codex backend also reports `cache_write_tokens` (observed
91
- * live on 2026-09-10). Declared here rather than cast at the use site, and
92
- * every field optional because a non-OpenAI host behind this SDK may report
93
- * neither.
132
+ * The SDK declares both counts on `ResponseUsage.InputTokensDetails` as of
133
+ * `openai` 7.x (6.x had `cached_tokens` alone, though the ChatGPT/Codex backend
134
+ * already reported `cache_write_tokens` — observed live on 2026-09-10). What it
135
+ * gets wrong for our purposes is that it types them as *required*: a
136
+ * non-OpenAI host behind this SDK — llama.cpp, vLLM, a gateway — may report
137
+ * one, the other, or neither. Restating them as optional keeps `parseUsage`
138
+ * from trusting a number that isn't there, and keeps `undefined` ("provider
139
+ * said nothing") distinct from `0` ("nothing was cached").
94
140
  */
95
141
  export type OpenAIInputTokensDetails = {
96
142
  /** Prompt tokens served from cache. */
97
143
  cached_tokens?: number;
98
- /** Prompt tokens written to cache. Codex backend only. */
144
+ /** Prompt tokens written to cache. On `gpt-5.6`+ these bill at 1.25x input. */
99
145
  cache_write_tokens?: number;
100
146
  };
101
147
  /**
@@ -230,6 +276,63 @@ export declare class OpenAiAgent<M extends OpenAIModel = OpenAIModel, TModelCard
230
276
  * emits `response.reasoning_summary_text.delta` events when it is set.
231
277
  */
232
278
  private buildReasoningParams;
279
+ /**
280
+ * Whether {@link AgentConfig.includeEncryptedReasoning} defaults to on, when
281
+ * the caller has not said either way.
282
+ *
283
+ * On only for OpenAI's own API *and* a model the reasoning table knows
284
+ * about. Asking a non-reasoning model for reasoning blobs would add a
285
+ * parameter it has nothing to put in; asking a third-party host behind a
286
+ * custom `baseURL` — a gateway, vLLM, llama.cpp — would silently start
287
+ * sending it a parameter it never received before, which is the opposite of
288
+ * the byte-identical request this default exists to preserve. A model name
289
+ * says nothing about the host serving it, so an OpenAI-shaped name on a proxy
290
+ * must not be enough on its own. Set the flag explicitly for a host that does
291
+ * support the round trip.
292
+ *
293
+ * `CodexAgent` overrides this: it always runs against a custom `baseURL`, and
294
+ * every model on that backend reasons.
295
+ *
296
+ * Called from the base constructor, so an override must depend on nothing but
297
+ * its arguments: the subclass's own fields are not assigned yet.
298
+ */
299
+ protected defaultIncludeEncryptedReasoning(model: string | undefined, baseURL: string | undefined): boolean;
300
+ /**
301
+ * The `include` field, asking for reasoning to come back in a form that can
302
+ * be replayed on the next request.
303
+ *
304
+ * Omitted entirely when off, so requests stay byte-identical to what earlier
305
+ * versions sent. The other half of this — putting the returned items back
306
+ * into `input` — is `openAiTransformer`'s, fed by
307
+ * {@link OpenAiAgent.replayableReasoning}.
308
+ */
309
+ private buildIncludeParams;
310
+ /**
311
+ * The `reasoning` items of a response that are worth keeping.
312
+ *
313
+ * Only items carrying `encrypted_content` qualify: the agent always sends
314
+ * `store: false`, so the provider has retained nothing, and an item replayed
315
+ * without its payload cannot be resolved — the request fails rather than
316
+ * silently ignoring it. A summary-only item is therefore dropped, exactly as
317
+ * it was before this existed.
318
+ *
319
+ * A turn that also used a **built-in tool** keeps its reasoning too. Those
320
+ * items (`web_search_call` and friends) are not stored, so the replayed turn
321
+ * is `[reasoning, reasoning, message]` where the model emitted
322
+ * `[reasoning, web_search_call, reasoning, message]`. That was expected to be
323
+ * rejected — "reasoning item without its required following item" — but it is
324
+ * not: probed live on 2026-09-10 against `gpt-5-nano` and `gpt-5.4-mini`, the
325
+ * API accepted that shape, the same reasoning item twice in a row, a
326
+ * reasoning item followed only by the next *user* turn, and a dangling
327
+ * reasoning item last in `input` with nothing after it at all. That ordering
328
+ * rule appears to govern the `store: true` / `previous_response_id` flow, not
329
+ * this one, which sends `store: false` and an explicit `input`.
330
+ *
331
+ * So the reasoning is kept. Dropping it would cost every `builtInTools` user
332
+ * their prompt-cache continuity to avoid a rejection that does not happen.
333
+ * Revisit if the API tightens.
334
+ */
335
+ protected replayableReasoning(response: Response): unknown[];
233
336
  /**
234
337
  * Prompt-caching parameters, omitted entirely when unconfigured so a request
235
338
  * stays byte-identical to what earlier versions sent.
@@ -107,7 +107,7 @@ function describeOpenAIError(error) {
107
107
  const body = err?.error;
108
108
  const fromBody = typeof body === "string"
109
109
  ? body
110
- : (body?.message ?? body?.detail ?? undefined);
110
+ : body?.message ?? body?.detail ?? undefined;
111
111
  return {
112
112
  message: fromBody ?? err?.detail ?? err?.message ?? "Unknown error",
113
113
  code: typeof body === "object" ? body?.code : undefined,
@@ -173,6 +173,9 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
173
173
  const builtInTools = config.builtInTools ?? vendorConfig.builtInTools;
174
174
  const promptCacheKey = config.promptCacheKey ?? vendorConfig.promptCacheKey;
175
175
  const promptCacheRetention = config.promptCacheRetention ?? vendorConfig.promptCacheRetention;
176
+ const includeEncryptedReasoning = config.includeEncryptedReasoning ??
177
+ vendorConfig.includeEncryptedReasoning ??
178
+ this.defaultIncludeEncryptedReasoning(config.model, baseURL);
176
179
  this.config = {
177
180
  model: config.model || "gpt-4.1-mini",
178
181
  // No default. `max_output_tokens` is optional on the Responses API, and
@@ -189,6 +192,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
189
192
  builtInTools,
190
193
  promptCacheKey,
191
194
  promptCacheRetention,
195
+ includeEncryptedReasoning,
192
196
  apiKey: config.apiKey,
193
197
  baseURL,
194
198
  temperature: config.temperature,
@@ -227,7 +231,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
227
231
  /** The configured key, resolving the function form if that is what was given. */
228
232
  async resolveApiKey() {
229
233
  const key = this.config.apiKey;
230
- return typeof key === "function" ? await key() : (key ?? "");
234
+ return typeof key === "function" ? await key() : key ?? "";
231
235
  }
232
236
  getToolDefinitions() {
233
237
  return Array.from(this.tools.values()).map((tool) => {
@@ -282,7 +286,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
282
286
  const output = streamedItems;
283
287
  const outputText = output
284
288
  .filter((item) => item.type === "message")
285
- .flatMap((item) => ("content" in item ? (item.content ?? []) : []))
289
+ .flatMap((item) => ("content" in item ? item.content ?? [] : []))
286
290
  .filter((part) => part?.type === "output_text")
287
291
  .map((part) => ("text" in part ? part.text : ""))
288
292
  .join("");
@@ -354,6 +358,77 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
354
358
  },
355
359
  };
356
360
  }
361
+ /**
362
+ * Whether {@link AgentConfig.includeEncryptedReasoning} defaults to on, when
363
+ * the caller has not said either way.
364
+ *
365
+ * On only for OpenAI's own API *and* a model the reasoning table knows
366
+ * about. Asking a non-reasoning model for reasoning blobs would add a
367
+ * parameter it has nothing to put in; asking a third-party host behind a
368
+ * custom `baseURL` — a gateway, vLLM, llama.cpp — would silently start
369
+ * sending it a parameter it never received before, which is the opposite of
370
+ * the byte-identical request this default exists to preserve. A model name
371
+ * says nothing about the host serving it, so an OpenAI-shaped name on a proxy
372
+ * must not be enough on its own. Set the flag explicitly for a host that does
373
+ * support the round trip.
374
+ *
375
+ * `CodexAgent` overrides this: it always runs against a custom `baseURL`, and
376
+ * every model on that backend reasons.
377
+ *
378
+ * Called from the base constructor, so an override must depend on nothing but
379
+ * its arguments: the subclass's own fields are not assigned yet.
380
+ */
381
+ defaultIncludeEncryptedReasoning(model, baseURL) {
382
+ return baseURL === undefined && lowestReasoningEffort(model) !== undefined;
383
+ }
384
+ /**
385
+ * The `include` field, asking for reasoning to come back in a form that can
386
+ * be replayed on the next request.
387
+ *
388
+ * Omitted entirely when off, so requests stay byte-identical to what earlier
389
+ * versions sent. The other half of this — putting the returned items back
390
+ * into `input` — is `openAiTransformer`'s, fed by
391
+ * {@link OpenAiAgent.replayableReasoning}.
392
+ */
393
+ buildIncludeParams() {
394
+ return this.config.includeEncryptedReasoning
395
+ ? { include: ["reasoning.encrypted_content"] }
396
+ : {};
397
+ }
398
+ /**
399
+ * The `reasoning` items of a response that are worth keeping.
400
+ *
401
+ * Only items carrying `encrypted_content` qualify: the agent always sends
402
+ * `store: false`, so the provider has retained nothing, and an item replayed
403
+ * without its payload cannot be resolved — the request fails rather than
404
+ * silently ignoring it. A summary-only item is therefore dropped, exactly as
405
+ * it was before this existed.
406
+ *
407
+ * A turn that also used a **built-in tool** keeps its reasoning too. Those
408
+ * items (`web_search_call` and friends) are not stored, so the replayed turn
409
+ * is `[reasoning, reasoning, message]` where the model emitted
410
+ * `[reasoning, web_search_call, reasoning, message]`. That was expected to be
411
+ * rejected — "reasoning item without its required following item" — but it is
412
+ * not: probed live on 2026-09-10 against `gpt-5-nano` and `gpt-5.4-mini`, the
413
+ * API accepted that shape, the same reasoning item twice in a row, a
414
+ * reasoning item followed only by the next *user* turn, and a dangling
415
+ * reasoning item last in `input` with nothing after it at all. That ordering
416
+ * rule appears to govern the `store: true` / `previous_response_id` flow, not
417
+ * this one, which sends `store: false` and an explicit `input`.
418
+ *
419
+ * So the reasoning is kept. Dropping it would cost every `builtInTools` user
420
+ * their prompt-cache continuity to avoid a rejection that does not happen.
421
+ * Revisit if the API tightens.
422
+ */
423
+ replayableReasoning(response) {
424
+ if (!this.config.includeEncryptedReasoning)
425
+ return [];
426
+ return (response.output ?? []).filter((item) => typeof item === "object" &&
427
+ item !== null &&
428
+ item.type === "reasoning" &&
429
+ typeof item.encrypted_content ===
430
+ "string");
431
+ }
357
432
  /**
358
433
  * Prompt-caching parameters, omitted entirely when unconfigured so a request
359
434
  * stays byte-identical to what earlier versions sent.
@@ -404,7 +479,9 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
404
479
  // mid-loop. endExecution() in the finally block enforces limits once.
405
480
  this.history.beginExecution();
406
481
  try {
407
- const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries());
482
+ const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries(), {
483
+ replayReasoning: this.config.includeEncryptedReasoning,
484
+ });
408
485
  this.startTurnTimer();
409
486
  const response = await this.createResponse({
410
487
  model: this.config.model,
@@ -418,6 +495,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
418
495
  user: this.config.user,
419
496
  ...this.buildReasoningParams(),
420
497
  ...this.buildCacheParams(),
498
+ ...this.buildIncludeParams(),
421
499
  }, { signal: options?.signal });
422
500
  this.emit(AgentEvent_1.AgentEvent.AFTER_EXECUTE, response);
423
501
  return await this.handleResponse(response, options);
@@ -494,7 +572,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
494
572
  messageOutput.type === "message" &&
495
573
  messageOutput.status === "completed") {
496
574
  // Normal text response - add to history in normalized format
497
- const entry = transformers_1.openAiTransformer.fromProviderMessage("assistant", response.output_text);
575
+ const entry = transformers_1.openAiTransformer.fromProviderMessage("assistant", response.output_text, undefined, this.replayableReasoning(response));
498
576
  this.addToHistory(entry);
499
577
  this.emit(AgentEvent_1.AgentEvent.DONE, response, this.lastTokenUsage);
500
578
  // Report completion to viz
@@ -522,7 +600,11 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
522
600
  name: tc.name,
523
601
  arguments: tc.arguments,
524
602
  }));
525
- const assistantEntry = transformers_1.openAiTransformer.fromProviderMessage("assistant", response.output_text || "", functionCalls);
603
+ const assistantEntry = transformers_1.openAiTransformer.fromProviderMessage("assistant", response.output_text || "", functionCalls,
604
+ // The thinking that led to these calls: replayed on the follow-up so
605
+ // the model does not have to re-derive it, and so the prefix the
606
+ // provider caches still matches what it processed.
607
+ this.replayableReasoning(response));
526
608
  this.addToHistory(assistantEntry);
527
609
  const toolResponses = await this.handleToolUse(toolCalls, options);
528
610
  // Add tool results to history (normalized)
@@ -532,7 +614,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
532
614
  }
533
615
  // Continue conversation
534
616
  try {
535
- const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries());
617
+ const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries(), { replayReasoning: this.config.includeEncryptedReasoning });
536
618
  this.startTurnTimer();
537
619
  const newResponse = await this.createResponse({
538
620
  model: this.config.model,
@@ -546,6 +628,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
546
628
  user: this.config.user,
547
629
  ...this.buildReasoningParams(),
548
630
  ...this.buildCacheParams(),
631
+ ...this.buildIncludeParams(),
549
632
  }, { signal: options?.signal });
550
633
  this.emit(AgentEvent_1.AgentEvent.AFTER_EXECUTE, newResponse);
551
634
  return this.handleResponse(newResponse, options);
@@ -714,9 +797,11 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
714
797
  }
715
798
  }
716
799
  async *streamTurn(options) {
717
- const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries());
800
+ const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries(), {
801
+ replayReasoning: this.config.includeEncryptedReasoning,
802
+ });
718
803
  this.startTurnTimer();
719
- const stream = await this.client.responses.create(this.transformRequestParams({
804
+ const stream = (await this.client.responses.create(this.transformRequestParams({
720
805
  model: this.config.model,
721
806
  max_output_tokens: this.config.maxTokens,
722
807
  input: inputMessages,
@@ -728,7 +813,8 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
728
813
  user: this.config.user,
729
814
  ...this.buildReasoningParams("auto"),
730
815
  ...this.buildCacheParams(),
731
- }), { signal: options?.signal });
816
+ ...this.buildIncludeParams(),
817
+ }), { signal: options?.signal }));
732
818
  let completedEvent = null;
733
819
  const streamedItems = [];
734
820
  // The Responses API builds the committed turn out of `response.completed`,
@@ -809,7 +895,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
809
895
  name: tc.name,
810
896
  arguments: tc.arguments,
811
897
  }));
812
- const assistantEntry = transformers_1.openAiTransformer.fromProviderMessage("assistant", response.output_text || "", functionCalls);
898
+ const assistantEntry = transformers_1.openAiTransformer.fromProviderMessage("assistant", response.output_text || "", functionCalls, this.replayableReasoning(response));
813
899
  this.addToHistory(assistantEntry);
814
900
  committed = true;
815
901
  const toolResults = await this.handleToolUse(toolCalls, options);
@@ -820,7 +906,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
820
906
  }
821
907
  else {
822
908
  const textContent = response.output_text || "";
823
- const entry = transformers_1.openAiTransformer.fromProviderMessage("assistant", textContent);
909
+ const entry = transformers_1.openAiTransformer.fromProviderMessage("assistant", textContent, undefined, this.replayableReasoning(response));
824
910
  this.addToHistory(entry);
825
911
  committed = true;
826
912
  this.emit(AgentEvent_1.AgentEvent.DONE, response, this.lastTokenUsage);
@@ -3,8 +3,8 @@ import { HistoryEntry, MessageRole, MessageContent } from "./types";
3
3
  import type { ReduceOptions } from "./types";
4
4
  /** @internal — exposed for test teardown only */
5
5
  export declare function resetTokenxCache(): void;
6
- export type { HistoryEntry, MessageRole, MessageContent, ReduceOptions, ImageMimeType, ImageUrlContent, ImageBase64Content, ThinkingContent, } from "./types";
7
- export { text, toolUse, toolResult, thinking, textMessage, imageUrl, imageBase64, isTextContent, isToolUseContent, isToolResultContent, isThinkingContent, isImageUrlContent, isImageBase64Content, isImageContent, } from "./types";
6
+ export type { HistoryEntry, MessageRole, MessageContent, ReduceOptions, ImageMimeType, ImageUrlContent, ImageBase64Content, ThinkingContent, ReasoningDetailsFormat, } from "./types";
7
+ export { text, toolUse, toolResult, thinking, textMessage, imageUrl, imageBase64, isTextContent, isToolUseContent, isToolResultContent, isThinkingContent, isImageUrlContent, isImageBase64Content, isImageContent, reasoningDetailsFormatOf, } from "./types";
8
8
  /**
9
9
  * Metadata stored alongside each history entry.
10
10
  * Extended with summary tracking fields for the compression plugin.
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.History = exports.isImageContent = exports.isImageBase64Content = exports.isImageUrlContent = exports.isThinkingContent = exports.isToolResultContent = exports.isToolUseContent = exports.isTextContent = exports.imageBase64 = exports.imageUrl = exports.textMessage = exports.thinking = exports.toolResult = exports.toolUse = exports.text = void 0;
39
+ exports.History = exports.reasoningDetailsFormatOf = exports.isImageContent = exports.isImageBase64Content = exports.isImageUrlContent = exports.isThinkingContent = exports.isToolResultContent = exports.isToolUseContent = exports.isTextContent = exports.imageBase64 = exports.imageUrl = exports.textMessage = exports.thinking = exports.toolResult = exports.toolUse = exports.text = void 0;
40
40
  exports.resetTokenxCache = resetTokenxCache;
41
41
  const events_1 = __importDefault(require("events"));
42
42
  const types_1 = require("./types");
@@ -53,9 +53,22 @@ void Promise.resolve().then(() => __importStar(require("tokenx"))).then((mod) =>
53
53
  function resetTokenxCache() {
54
54
  _estimateTokenCount = (t) => Math.ceil(t.length / 4);
55
55
  }
56
+ /**
57
+ * Chars per token for an opaque reasoning payload.
58
+ *
59
+ * `ThinkingContent.reasoningDetails` holds ciphertext — OpenAI's
60
+ * `encrypted_content`, OpenRouter's `reasoning.encrypted` — which stands in for
61
+ * the reasoning tokens the provider decrypts it back into, and is far longer
62
+ * than the tokens it represents. Running it through the text estimator counted
63
+ * it at roughly three times its weight, which trimmed history early for no
64
+ * reason. Measured on a Codex turn: 1892 chars of blob carried 156 reasoning
65
+ * tokens, i.e. ~12 chars per token against the ~4 a plain string averages.
66
+ */
67
+ const CHARS_PER_ENCRYPTED_REASONING_TOKEN = 12;
56
68
  /**
57
69
  * Estimate token count for a content block array.
58
70
  * Image blocks use a flat 1000-token estimate (resolution-independent conservative value).
71
+ * Encrypted reasoning payloads get their own ratio, see above.
59
72
  * Text and tool blocks fall through to the tokenx estimator.
60
73
  */
61
74
  function estimateContentTokens(content) {
@@ -63,6 +76,13 @@ function estimateContentTokens(content) {
63
76
  if ((0, types_1.isImageContent)(block)) {
64
77
  return sum + 1000;
65
78
  }
79
+ if ((0, types_1.isThinkingContent)(block) && block.reasoningDetails?.length) {
80
+ const { reasoningDetails, ...rest } = block;
81
+ return (sum +
82
+ _estimateTokenCount(JSON.stringify(rest)) +
83
+ Math.ceil(JSON.stringify(reasoningDetails).length /
84
+ CHARS_PER_ENCRYPTED_REASONING_TOKEN));
85
+ }
66
86
  return sum + _estimateTokenCount(JSON.stringify(block));
67
87
  }, 0);
68
88
  }
@@ -81,6 +101,7 @@ Object.defineProperty(exports, "isThinkingContent", { enumerable: true, get: fun
81
101
  Object.defineProperty(exports, "isImageUrlContent", { enumerable: true, get: function () { return types_2.isImageUrlContent; } });
82
102
  Object.defineProperty(exports, "isImageBase64Content", { enumerable: true, get: function () { return types_2.isImageBase64Content; } });
83
103
  Object.defineProperty(exports, "isImageContent", { enumerable: true, get: function () { return types_2.isImageContent; } });
104
+ Object.defineProperty(exports, "reasoningDetailsFormatOf", { enumerable: true, get: function () { return types_2.reasoningDetailsFormatOf; } });
84
105
  /**
85
106
  * Manages conversation history in a provider-agnostic format.
86
107
  *
@@ -433,7 +454,8 @@ class History extends events_1.default {
433
454
  applyTrimming() {
434
455
  if (this._executing)
435
456
  return;
436
- if (this.options.maxLength && this._entries.length > this.options.maxLength) {
457
+ if (this.options.maxLength &&
458
+ this._entries.length > this.options.maxLength) {
437
459
  this._entries = this._entries.slice(this._entries.length - this.options.maxLength);
438
460
  this.sanitizeToolPairs();
439
461
  }
@@ -1,5 +1,5 @@
1
1
  export { History, resetTokenxCache, type EntryMetadata, type ReducibleEntry, type HistoryPlugin, } from "./History";
2
2
  export { RedisHistory } from "./RedisHistory";
3
- export type { HistoryEntry, MessageRole, MessageContent, TextContent, ToolUseContent, ToolResultContent, ThinkingContent, ProviderMeta, ReduceOptions, } from "./types";
4
- export { text, toolUse, toolResult, thinking, textMessage, isTextContent, isToolUseContent, isToolResultContent, isThinkingContent, } from "./types";
3
+ export type { HistoryEntry, MessageRole, MessageContent, TextContent, ToolUseContent, ToolResultContent, ThinkingContent, ReasoningDetailsFormat, ProviderMeta, ReduceOptions, } from "./types";
4
+ export { text, toolUse, toolResult, thinking, textMessage, isTextContent, isToolUseContent, isToolResultContent, isThinkingContent, reasoningDetailsFormatOf, } from "./types";
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isThinkingContent = exports.isToolResultContent = exports.isToolUseContent = exports.isTextContent = exports.textMessage = exports.thinking = exports.toolResult = exports.toolUse = exports.text = exports.RedisHistory = exports.resetTokenxCache = exports.History = void 0;
3
+ exports.reasoningDetailsFormatOf = exports.isThinkingContent = exports.isToolResultContent = exports.isToolUseContent = exports.isTextContent = exports.textMessage = exports.thinking = exports.toolResult = exports.toolUse = exports.text = exports.RedisHistory = exports.resetTokenxCache = exports.History = void 0;
4
4
  var History_1 = require("./History");
5
5
  Object.defineProperty(exports, "History", { enumerable: true, get: function () { return History_1.History; } });
6
6
  Object.defineProperty(exports, "resetTokenxCache", { enumerable: true, get: function () { return History_1.resetTokenxCache; } });
@@ -16,4 +16,5 @@ Object.defineProperty(exports, "isTextContent", { enumerable: true, get: functio
16
16
  Object.defineProperty(exports, "isToolUseContent", { enumerable: true, get: function () { return types_1.isToolUseContent; } });
17
17
  Object.defineProperty(exports, "isToolResultContent", { enumerable: true, get: function () { return types_1.isToolResultContent; } });
18
18
  Object.defineProperty(exports, "isThinkingContent", { enumerable: true, get: function () { return types_1.isThinkingContent; } });
19
+ Object.defineProperty(exports, "reasoningDetailsFormatOf", { enumerable: true, get: function () { return types_1.reasoningDetailsFormatOf; } });
19
20
  //# sourceMappingURL=index.js.map
@@ -23,9 +23,18 @@ export declare const anthropicTransformer: {
23
23
  };
24
24
  export declare const openAiTransformer: {
25
25
  /**
26
- * Convert normalized entries to OpenAI ResponseInputItem format
26
+ * Convert normalized entries to OpenAI ResponseInputItem format.
27
+ *
28
+ * `replayReasoning` defaults to on and mirrors
29
+ * `AgentConfig.includeEncryptedReasoning`: requesting the blobs and sending
30
+ * them back are one feature, so switching it off has to stop *both*.
31
+ * Otherwise a history that already holds blobs keeps replaying them — which
32
+ * is exactly the situation the flag is turned off to escape, since reasoning
33
+ * is tied to the model that produced it and switching models is rejected.
27
34
  */
28
- toProvider(entries: HistoryEntry[]): ResponseInputItem[];
35
+ toProvider(entries: HistoryEntry[], options?: {
36
+ replayReasoning?: boolean;
37
+ }): ResponseInputItem[];
29
38
  /**
30
39
  * Convert OpenAI response to normalized HistoryEntry
31
40
  */
@@ -34,7 +43,7 @@ export declare const openAiTransformer: {
34
43
  call_id: string;
35
44
  name: string;
36
45
  arguments: string;
37
- }>): HistoryEntry;
46
+ }>, reasoningItems?: unknown[]): HistoryEntry;
38
47
  /**
39
48
  * Create a tool result entry from OpenAI function call output
40
49
  */
@@ -10,17 +10,33 @@ const types_1 = require("./types");
10
10
  // =============================================================================
11
11
  // Anthropic Transformer
12
12
  // =============================================================================
13
+ /**
14
+ * Whether a thinking block is one Anthropic itself produced, and so can be sent
15
+ * back to it.
16
+ *
17
+ * Anthropic signs every thinking block it emits (or returns it redacted), and
18
+ * rejects one whose signature does not verify — an empty string included. Other
19
+ * providers' reasoning reaches this transformer whenever a `History` is shared
20
+ * between agents, which the docs recommend: OpenAI's carries a summary and an
21
+ * encrypted payload but no signature, so it has to be dropped rather than
22
+ * replayed under an empty one.
23
+ */
24
+ function isAnthropicThinkingBlock(block) {
25
+ return block.redactedData !== undefined || block.signature !== undefined;
26
+ }
13
27
  exports.anthropicTransformer = {
14
28
  /**
15
29
  * Convert normalized entries to Anthropic MessageParam format
16
30
  */
17
31
  toProvider(entries) {
18
- return entries
32
+ return (entries
19
33
  .filter((entry) => entry.role !== "system") // Anthropic handles system separately
20
34
  .map((entry) => {
21
35
  const role = entry.role === "assistant" ? "assistant" : "user";
22
36
  // Convert content blocks to Anthropic's ContentBlockParam
23
- const content = entry.content.map((block) => {
37
+ const content = entry.content
38
+ .filter((block) => !(0, types_1.isThinkingContent)(block) || isAnthropicThinkingBlock(block))
39
+ .map((block) => {
24
40
  if ((0, types_1.isTextContent)(block)) {
25
41
  return { type: "text", text: block.text };
26
42
  }
@@ -72,7 +88,12 @@ exports.anthropicTransformer = {
72
88
  throw new Error(`Unknown content type: ${block.type}`);
73
89
  });
74
90
  return { role, content };
75
- });
91
+ })
92
+ // Dropping a foreign thinking block can empty a turn that held nothing
93
+ // else (a reasoning-only assistant entry). Anthropic rejects a message
94
+ // with no content, and there is nothing left to say, so drop the message
95
+ // too. Tool pairs are unaffected: a thinking-only entry has no tool_use.
96
+ .filter((message) => message.content.length > 0));
76
97
  },
77
98
  /**
78
99
  * Convert Anthropic response content to normalized HistoryEntry
@@ -139,12 +160,48 @@ function toOpenAiId(originalId) {
139
160
  idMappingToOpenAi.set(originalId, newId);
140
161
  return newId;
141
162
  }
163
+ function isOpenAiReasoningItem(value) {
164
+ return (typeof value === "object" &&
165
+ value !== null &&
166
+ value.type === "reasoning");
167
+ }
168
+ /**
169
+ * The reasoning items stored on an assistant entry, in the order they were
170
+ * received.
171
+ *
172
+ * They ride on `ThinkingContent.reasoningDetails` — the same passthrough slot
173
+ * OpenRouter's `reasoning_details` uses — because they have to survive the round
174
+ * trip untouched and there is nothing to model.
175
+ */
176
+ function openAiReasoningItems(entry) {
177
+ return entry.content
178
+ .filter(types_1.isThinkingContent)
179
+ .filter((block) => (0, types_1.reasoningDetailsFormatOf)(block) === "openai.responses")
180
+ .flatMap((block) => block.reasoningDetails ?? [])
181
+ .filter(isOpenAiReasoningItem);
182
+ }
183
+ /** Human-readable text of a reasoning item's summary, for display in history. */
184
+ function reasoningSummaryText(items) {
185
+ return items
186
+ .flatMap((item) => item.summary ?? [])
187
+ .map((part) => part?.text ?? "")
188
+ .filter(Boolean)
189
+ .join("\n\n");
190
+ }
142
191
  exports.openAiTransformer = {
143
192
  /**
144
- * Convert normalized entries to OpenAI ResponseInputItem format
193
+ * Convert normalized entries to OpenAI ResponseInputItem format.
194
+ *
195
+ * `replayReasoning` defaults to on and mirrors
196
+ * `AgentConfig.includeEncryptedReasoning`: requesting the blobs and sending
197
+ * them back are one feature, so switching it off has to stop *both*.
198
+ * Otherwise a history that already holds blobs keeps replaying them — which
199
+ * is exactly the situation the flag is turned off to escape, since reasoning
200
+ * is tied to the model that produced it and switching models is rejected.
145
201
  */
146
- toProvider(entries) {
202
+ toProvider(entries, options) {
147
203
  const items = [];
204
+ const replayReasoning = options?.replayReasoning ?? true;
148
205
  for (const entry of entries) {
149
206
  if (entry.role === "system") {
150
207
  items.push({
@@ -157,6 +214,18 @@ exports.openAiTransformer = {
157
214
  });
158
215
  continue;
159
216
  }
217
+ // Reasoning first, before anything else this turn produced — the order
218
+ // the model emitted it in. The Responses API turns out not to enforce
219
+ // this on the `store: false` + explicit-`input` flow these agents use
220
+ // (probed live 2026-09-10; see `OpenAiAgent.replayableReasoning` for what
221
+ // was accepted), so this is fidelity to what the model produced rather
222
+ // than a constraint. Emitting it out of order is not known to fail, but
223
+ // there is no reason to.
224
+ if (replayReasoning) {
225
+ for (const item of openAiReasoningItems(entry)) {
226
+ items.push(item);
227
+ }
228
+ }
160
229
  // Separate content blocks by type for OpenAI format
161
230
  const textBlocks = entry.content.filter(types_1.isTextContent);
162
231
  const toolUseBlocks = entry.content.filter(types_1.isToolUseContent);
@@ -238,8 +307,21 @@ exports.openAiTransformer = {
238
307
  /**
239
308
  * Convert OpenAI response to normalized HistoryEntry
240
309
  */
241
- fromProviderMessage(role, outputText, functionCalls) {
310
+ fromProviderMessage(role, outputText, functionCalls,
311
+ /**
312
+ * `reasoning` items from `response.output`, stored verbatim so the next
313
+ * request can replay them. Pass only items that carry `encrypted_content`:
314
+ * with `store: false` the provider has kept nothing of its own, so a
315
+ * replayed item without it cannot be resolved and the request is rejected.
316
+ */
317
+ reasoningItems) {
242
318
  const content = [];
319
+ const reasoning = (reasoningItems ?? []).filter(isOpenAiReasoningItem);
320
+ if (reasoning.length > 0) {
321
+ // Summary text is for humans reading the history; the payload that
322
+ // matters is the untouched items on `reasoningDetails`.
323
+ content.push((0, types_1.thinking)(reasoningSummaryText(reasoning), undefined, undefined, reasoning, "openai.responses"));
324
+ }
243
325
  if (outputText) {
244
326
  content.push((0, types_1.text)(outputText));
245
327
  }
@@ -553,7 +635,10 @@ exports.ollamaTransformer = {
553
635
  const toolUseBlocks = entry.content.filter(types_1.isToolUseContent);
554
636
  const toolResultBlocks = entry.content.filter(types_1.isToolResultContent);
555
637
  if (entry.role === "system") {
556
- messages.push({ role: "system", content: textBlocks.map((c) => c.text).join("\n") });
638
+ messages.push({
639
+ role: "system",
640
+ content: textBlocks.map((c) => c.text).join("\n"),
641
+ });
557
642
  continue;
558
643
  }
559
644
  if (entry.role === "assistant") {
@@ -579,7 +664,10 @@ exports.ollamaTransformer = {
579
664
  }
580
665
  }
581
666
  else if (textBlocks.length > 0) {
582
- messages.push({ role: "user", content: textBlocks.map((c) => c.text).join("\n") });
667
+ messages.push({
668
+ role: "user",
669
+ content: textBlocks.map((c) => c.text).join("\n"),
670
+ });
583
671
  }
584
672
  }
585
673
  return messages;
@@ -650,7 +738,10 @@ exports.chatCompletionsTransformer = {
650
738
  const imageBase64Blocks = entry.content.filter(types_1.isImageBase64Content);
651
739
  const hasImages = imageUrlBlocks.length > 0 || imageBase64Blocks.length > 0;
652
740
  if (entry.role === "system") {
653
- messages.push({ role: "system", content: textBlocks.map((c) => c.text).join("\n") });
741
+ messages.push({
742
+ role: "system",
743
+ content: textBlocks.map((c) => c.text).join("\n"),
744
+ });
654
745
  continue;
655
746
  }
656
747
  if (entry.role === "assistant") {
@@ -720,7 +811,10 @@ exports.chatCompletionsTransformer = {
720
811
  messages.push({ role: "user", content: parts });
721
812
  }
722
813
  else if (textBlocks.length > 0) {
723
- messages.push({ role: "user", content: textBlocks.map((c) => c.text).join("\n") });
814
+ messages.push({
815
+ role: "user",
816
+ content: textBlocks.map((c) => c.text).join("\n"),
817
+ });
724
818
  }
725
819
  }
726
820
  return messages;
@@ -807,7 +901,11 @@ function markLatestCacheBreakpoint(messages) {
807
901
  const message = messages[i];
808
902
  if (typeof message.content === "string" && message.content.length > 0) {
809
903
  message.content = [
810
- { type: "text", text: message.content, cacheControl: { type: "ephemeral" } },
904
+ {
905
+ type: "text",
906
+ text: message.content,
907
+ cacheControl: { type: "ephemeral" },
908
+ },
811
909
  ];
812
910
  return;
813
911
  }
@@ -861,7 +959,15 @@ exports.openRouterTransformer = {
861
959
  // and Anthropic and OpenAI models reject a tool-using turn whose
862
960
  // signature did not come back. Only set the key when there is one, so
863
961
  // requests for non-reasoning models stay byte-identical.
864
- const reasoningDetails = thinkingBlocks.flatMap((block) => block.reasoningDetails ?? []);
962
+ // Only OpenRouter's own blocks. A history shared with an OpenAI agent
963
+ // also carries Responses-API `reasoning` items in this slot, and they
964
+ // are not OpenRouter's to interpret — it returned 200 rather than an
965
+ // error when one was sent (probed live 2026-09-10, upstream
966
+ // `anthropic/claude-sonnet-4.5`), but forwarding another provider's
967
+ // opaque payload can only confuse the model or the upstream route.
968
+ const reasoningDetails = thinkingBlocks
969
+ .filter((block) => (0, types_1.reasoningDetailsFormatOf)(block) === "openrouter")
970
+ .flatMap((block) => block.reasoningDetails ?? []);
865
971
  if (reasoningDetails.length > 0) {
866
972
  msg.reasoningDetails = reasoningDetails;
867
973
  }
@@ -934,7 +1040,7 @@ exports.openRouterTransformer = {
934
1040
  const reasoningText = typeof message.reasoning === "string" ? message.reasoning : "";
935
1041
  const reasoningDetails = message.reasoningDetails ?? [];
936
1042
  if (reasoningText || reasoningDetails.length > 0) {
937
- content.push((0, types_1.thinking)(reasoningText, undefined, undefined, reasoningDetails));
1043
+ content.push((0, types_1.thinking)(reasoningText, undefined, undefined, reasoningDetails, "openrouter"));
938
1044
  }
939
1045
  if (typeof message.content === "string" && message.content) {
940
1046
  content.push((0, types_1.text)(message.content));
@@ -62,9 +62,32 @@ export type ThinkingContent = {
62
62
  *
63
63
  * Nothing here reads the contents; they only have to survive the round trip,
64
64
  * so they stay untyped rather than modelling every provider's block shapes.
65
+ *
66
+ * The shapes are *not* interchangeable — see
67
+ * {@link ThinkingContent.reasoningDetailsFormat}.
65
68
  */
66
69
  reasoningDetails?: unknown[];
70
+ /**
71
+ * Which provider's format {@link ThinkingContent.reasoningDetails} is in.
72
+ *
73
+ * Two producers share that slot and their shapes are mutually invalid:
74
+ * OpenRouter's entries are tagged `reasoning.text` / `reasoning.summary` /
75
+ * `reasoning.encrypted`, while the OpenAI Responses API's are whole
76
+ * `reasoning` items. A history shared between agents therefore has to say
77
+ * which is which, or each transformer forwards the other's blocks and the
78
+ * provider rejects the request.
79
+ *
80
+ * Absent on blocks written before this field existed, which are OpenRouter's
81
+ * by definition — it was the only producer then. Read it through
82
+ * {@link reasoningDetailsFormatOf} rather than directly, so that default
83
+ * stays in one place.
84
+ */
85
+ reasoningDetailsFormat?: ReasoningDetailsFormat;
67
86
  };
87
+ /**
88
+ * Provider formats that {@link ThinkingContent.reasoningDetails} can hold.
89
+ */
90
+ export type ReasoningDetailsFormat = "openrouter" | "openai.responses";
68
91
  /**
69
92
  * Supported image MIME types across all providers
70
93
  */
@@ -207,7 +230,14 @@ export declare function toolUse(id: string, name: string, input: Record<string,
207
230
  /**
208
231
  * Create a thinking content block. Pass `redactedData` for redacted thinking.
209
232
  */
210
- export declare function thinking(thinkingText: string, signature?: string, redactedData?: string, reasoningDetails?: unknown[]): ThinkingContent;
233
+ export declare function thinking(thinkingText: string, signature?: string, redactedData?: string, reasoningDetails?: unknown[], reasoningDetailsFormat?: ReasoningDetailsFormat): ThinkingContent;
234
+ /**
235
+ * The format of a block's {@link ThinkingContent.reasoningDetails}.
236
+ *
237
+ * Untagged blocks are OpenRouter's: it was the only producer before the tag
238
+ * existed, so that is what an untagged block can only have come from.
239
+ */
240
+ export declare function reasoningDetailsFormatOf(block: ThinkingContent): ReasoningDetailsFormat;
211
241
  /**
212
242
  * Create a tool result content block
213
243
  */
@@ -16,6 +16,7 @@ exports.isImageContent = isImageContent;
16
16
  exports.text = text;
17
17
  exports.toolUse = toolUse;
18
18
  exports.thinking = thinking;
19
+ exports.reasoningDetailsFormatOf = reasoningDetailsFormatOf;
19
20
  exports.toolResult = toolResult;
20
21
  exports.textMessage = textMessage;
21
22
  exports.imageUrl = imageUrl;
@@ -70,20 +71,29 @@ function toolUse(id, name, input, thoughtSignature) {
70
71
  /**
71
72
  * Create a thinking content block. Pass `redactedData` for redacted thinking.
72
73
  */
73
- function thinking(thinkingText, signature, redactedData, reasoningDetails) {
74
+ function thinking(thinkingText, signature, redactedData, reasoningDetails, reasoningDetailsFormat = "openrouter") {
74
75
  // As in `toolUse()`, only set the passthrough key when there is something in
75
76
  // it, so a block stored without details serializes exactly as it did before
76
- // the field existed.
77
+ // the field existed. The format tag rides along with the details for the same
78
+ // reason: it says nothing on its own.
79
+ const hasDetails = reasoningDetails !== undefined && reasoningDetails.length > 0;
77
80
  return {
78
81
  type: "thinking",
79
82
  thinking: thinkingText,
80
83
  signature,
81
84
  redactedData,
82
- ...(reasoningDetails && reasoningDetails.length > 0
83
- ? { reasoningDetails }
84
- : {}),
85
+ ...(hasDetails ? { reasoningDetails, reasoningDetailsFormat } : {}),
85
86
  };
86
87
  }
88
+ /**
89
+ * The format of a block's {@link ThinkingContent.reasoningDetails}.
90
+ *
91
+ * Untagged blocks are OpenRouter's: it was the only producer before the tag
92
+ * existed, so that is what an untagged block can only have come from.
93
+ */
94
+ function reasoningDetailsFormatOf(block) {
95
+ return block.reasoningDetailsFormat ?? "openrouter";
96
+ }
87
97
  /**
88
98
  * Create a tool result content block
89
99
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agentionai/agents",
3
3
  "author": "Laurent Zuijdwijk",
4
- "version": "1.14.0",
4
+ "version": "1.15.0",
5
5
  "description": "Agent Library",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -132,7 +132,7 @@
132
132
  "jsdoc": "^4.0.4",
133
133
  "jsdoc-to-markdown": "^9.1.1",
134
134
  "nodemon": "^2.0.22",
135
- "openai": "^6.16.0",
135
+ "openai": "^7.13.0",
136
136
  "prettier": "^2.8.7",
137
137
  "rimraf": "^4.4.1",
138
138
  "ts-jest": "^29.2.6",
@@ -153,7 +153,7 @@
153
153
  "@openrouter/sdk": "^1.2.106",
154
154
  "apache-arrow": "^18.0.0",
155
155
  "ollama": "^0.5.18",
156
- "openai": "^6.16.0",
156
+ "openai": "^7.13.0",
157
157
  "voyageai": "^0.0.3"
158
158
  },
159
159
  "peerDependenciesMeta": {