@hiper2d/ai-agents 0.13.0 → 0.14.1

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.
package/dist/index.mjs CHANGED
@@ -1527,6 +1527,9 @@ function getProviderSignatureFields(aiType, signature) {
1527
1527
  if (aiType.startsWith("grok")) {
1528
1528
  return { grokEncryptedReasoning: signature };
1529
1529
  }
1530
+ if (aiType.startsWith("gpt")) {
1531
+ return { openaiEncryptedReasoning: signature };
1532
+ }
1530
1533
  if (aiType.startsWith("muse-")) {
1531
1534
  return { metaEncryptedReasoning: signature };
1532
1535
  }
@@ -2351,7 +2354,54 @@ ${instruction}`);
2351
2354
  * inherited from a default OpenAI can change under us.
2352
2355
  */
2353
2356
  reasoningParams() {
2354
- return this.reasoningEffort ? { reasoning: { effort: toOpenAIEffort(this.reasoningEffort) } } : {};
2357
+ return {
2358
+ reasoning: {
2359
+ ...this.reasoningEffort ? { effort: toOpenAIEffort(this.reasoningEffort) } : {},
2360
+ // Render reasoning from earlier turns into this sample. It is already the
2361
+ // default on all four models we run, but stated explicitly for the same reason
2362
+ // effort is. It only does anything because we replay the items ourselves.
2363
+ context: "all_turns"
2364
+ }
2365
+ };
2366
+ }
2367
+ /**
2368
+ * Responses API input items, with each assistant turn's stored reasoning items replayed
2369
+ * immediately before it. OpenAI attaches `encrypted_content` to reasoning items whenever
2370
+ * `store` is false, which is how we call it: the documented stateless route is to preserve
2371
+ * the output items and replay the history yourself, rather than lean on
2372
+ * `previous_response_id` and OpenAI-side session state.
2373
+ *
2374
+ * This was a single flattened string before ("User: ... / Assistant: ..."), which left
2375
+ * nowhere to put reasoning items. The system prompt still travels ONLY as `instructions`.
2376
+ *
2377
+ * Reasoning is readable only within one model family and the API silently omits items it
2378
+ * cannot read, so a bot moved between GPT-6 and GPT-5.6 Terra loses its reasoning and keeps
2379
+ * working. A bot moved to another provider never reaches this code at all: every agent
2380
+ * reads only its own field, and a missing one falls back to a plain message.
2381
+ */
2382
+ buildResponsesInput(messages) {
2383
+ const input = [];
2384
+ for (const msg of this.prepareMessages(messages)) {
2385
+ if (msg.role === "assistant" && msg.openaiEncryptedReasoning) {
2386
+ try {
2387
+ const items = JSON.parse(msg.openaiEncryptedReasoning);
2388
+ if (Array.isArray(items)) {
2389
+ input.push(...items);
2390
+ }
2391
+ } catch {
2392
+ this.logger("Failed to parse stored encrypted reasoning, replaying message without it");
2393
+ }
2394
+ }
2395
+ input.push({ role: msg.role, content: msg.content });
2396
+ }
2397
+ return input;
2398
+ }
2399
+ /** The response's reasoning items, JSON-serialized for the caller to store on the message. */
2400
+ extractReasoningItems(response) {
2401
+ const items = (response?.output ?? []).filter(
2402
+ (item) => item?.type === "reasoning" && item.encrypted_content
2403
+ );
2404
+ return items.length > 0 ? JSON.stringify(items) : void 0;
2355
2405
  }
2356
2406
  /**
2357
2407
  * Structured output method using Zod with OpenAI's Responses API
@@ -2363,7 +2413,7 @@ ${instruction}`);
2363
2413
  try {
2364
2414
  this.logAsking(messages);
2365
2415
  this.logMessages(messages);
2366
- const input = this.prepareMessages(messages).map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`).join("\n\n");
2416
+ const input = this.buildResponsesInput(messages);
2367
2417
  const schemaToSend = zodSchema;
2368
2418
  let response;
2369
2419
  try {
@@ -2373,6 +2423,8 @@ ${instruction}`);
2373
2423
  input,
2374
2424
  max_output_tokens: this.maxOutputTokens,
2375
2425
  prompt_cache_key: this.promptCacheKey,
2426
+ // Stateless: encrypted reasoning is only returned for unstored responses.
2427
+ store: false,
2376
2428
  ...this.reasoningParams(),
2377
2429
  text: {
2378
2430
  format: zodTextFormat(schemaToSend, "response_schema")
@@ -2433,7 +2485,7 @@ ${instruction}`);
2433
2485
  this.logReply(response.output_parsed, reasoningContent, tokenUsage);
2434
2486
  }
2435
2487
  this.logger(`\u2705 Response validated successfully with Zod schema`);
2436
- return [response.output_parsed, reasoningContent, tokenUsage];
2488
+ return [response.output_parsed, reasoningContent, tokenUsage, this.extractReasoningItems(response)];
2437
2489
  } catch (error) {
2438
2490
  this.logger(this.logTemplates.error(this.name, error));
2439
2491
  if (error instanceof ModelError) {
@@ -2452,13 +2504,14 @@ ${instruction}`);
2452
2504
  try {
2453
2505
  this.logAsking(messages);
2454
2506
  this.logMessages(messages);
2455
- const input = this.prepareMessages(messages).map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`).join("\n\n");
2507
+ const input = this.buildResponsesInput(messages);
2456
2508
  const response = await this.client.responses.create({
2457
2509
  model: this.model,
2458
2510
  instructions: this.instruction,
2459
2511
  input,
2460
2512
  max_output_tokens: this.maxOutputTokens,
2461
2513
  prompt_cache_key: this.promptCacheKey,
2514
+ store: false,
2462
2515
  ...this.reasoningParams()
2463
2516
  });
2464
2517
  const content = response.output_text;
@@ -2500,7 +2553,7 @@ ${instruction}`);
2500
2553
  }
2501
2554
  }
2502
2555
  this.logReply(content, "", tokenUsage);
2503
- return [content, "", tokenUsage];
2556
+ return [content, "", tokenUsage, this.extractReasoningItems(response)];
2504
2557
  } catch (error) {
2505
2558
  this.logger(this.logTemplates.error(this.name, error));
2506
2559
  if (error instanceof ModelError) {
@@ -4572,24 +4625,49 @@ var QwenAgent = class extends AbstractAgent {
4572
4625
  * generation); without it the model thinks at the provider default, and qwen3.8-max's
4573
4626
  * latency then swings 30–100s.
4574
4627
  *
4575
- * `reasoning_effort` is deliberately NOT sent. Probed live 2026-08-30 on qwen3.8-flash and
4576
- * qwen3.8-max: every value low..max is accepted, but reasoning length doesn't track it
4577
- * (max: low → 1,686 reasoning tokens / 44s, high → 226 / 7s, xhigh → 1,102 / 30s), while
4578
- * thinking_budget bounds it reliably (≤340 at 1024). The docs also call the two mutually
4579
- * exclusive on qwen3.8-max. So on Qwen the budget IS the effort knob; `reasoningEffort`
4580
- * on this agent is ignored.
4628
+ * `reasoning_effort` is NOT sent, and the earlier reason recorded here was wrong. The
4629
+ * 2026-08-30 probe concluded "reasoning length doesn't track effort"; it had tested
4630
+ * low/high/max/xhigh, but Qwen only defines low|medium|xhigh and aliases both `high` and
4631
+ * `max` onto `xhigh`. So that probe compared low against xhigh twice and read the noise as
4632
+ * non-monotonicity.
4633
+ *
4634
+ * Re-probed 2026-09-22 with the documented levels, and effort tracks cleanly:
4635
+ * qwen3.8-max budget 1024 → 77 reasoning | low 132 | medium 160 | xhigh 422
4636
+ * qwen3.8-flash budget 1024 → 86 reasoning | low 87 | medium 251 | xhigh 417
4637
+ *
4638
+ * We still send the budget, for a better reason: the two are mutually exclusive (sending
4639
+ * both is a 400), Qwen documents effort as a coarse alias for a budget (low = 4,096,
4640
+ * medium = 16,384, xhigh = 262,144 tokens), and our 1,024 is tighter than the lowest level
4641
+ * it can express. The budget is the finer AND cheaper knob here, so `reasoningEffort` on
4642
+ * this agent stays ignored — deliberately, not because effort is broken.
4581
4643
  */
4582
4644
  thinkingParams() {
4583
4645
  const budget = this.thinkingBudgetTokens;
4584
4646
  return {
4585
4647
  enable_thinking: this.enableThinking,
4586
- ...this.enableThinking && budget !== void 0 ? { thinking_budget: budget } : {}
4648
+ ...this.enableThinking && budget !== void 0 ? { thinking_budget: budget } : {},
4649
+ // Lets the model read the `reasoning_content` we replay on assistant messages, so a
4650
+ // bot keeps its own train of thought across turns. Qwen documents models as NOT
4651
+ // reading replayed reasoning unless this is set (qwen3.8-max and qwen3.8-flash are
4652
+ // both on the supported list). Measured 2026-09-22: qwen3.8-max actually used the
4653
+ // replayed reasoning with the flag off too, but we follow the documented contract
4654
+ // rather than rely on that. Stateless — the reasoning comes from our payload, not
4655
+ // from any server-side session.
4656
+ ...this.enableThinking ? { preserve_thinking: true } : {}
4587
4657
  };
4588
4658
  }
4659
+ /**
4660
+ * Assistant turns carry their own `reasoning_content` back so the model can reference what
4661
+ * it was thinking on earlier turns (paired with `preserve_thinking` in thinkingParams).
4662
+ * Qwen's reasoning is PLAIN TEXT, not a signed or encrypted blob, so there is nothing for
4663
+ * another provider to reject: a bot whose model changes mid-game simply stops sending it
4664
+ * and keeps working. The text is the same one the caller already stores as `thinking`.
4665
+ */
4589
4666
  convertToOpenAIMessages(messages) {
4590
4667
  return messages.map((msg) => ({
4591
4668
  role: msg.role,
4592
- content: msg.content
4669
+ content: msg.content,
4670
+ ...msg.role === "assistant" && msg.thinking ? { reasoning_content: msg.thinking } : {}
4593
4671
  }));
4594
4672
  }
4595
4673
  extractThinkingAndUsage(completion) {