@hiper2d/ai-agents 0.13.1 → 0.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.
package/dist/index.mjs CHANGED
@@ -1065,10 +1065,15 @@ var SupportedAiModels = {
1065
1065
  // Live 2026-08-30 (one sample each): day-2 vote 11.8s, 15-character story 56.2s.
1066
1066
  tags: ["cheap"]
1067
1067
  },
1068
- // Sakana Fugu models — OpenAI-compatible. They reason internally (and bill it as
1069
- // "orchestration" tokens), but never surface reasoning to us: responses come back with
1070
- // reasoning_tokens: 0 and no reasoning_content. So hasThinking is false — there's no
1071
- // thinking content to show and no user-facing thinking toggle. Single entry per model.
1068
+ // Sakana Fugu models — OpenAI-compatible. They reason internally, but Chat Completions
1069
+ // (what FuguAgent uses) never returns the text: reasoning_tokens are reported and billed,
1070
+ // there is no reasoning_content. So hasThinking is false. Single entry per model.
1071
+ // The Messages and Responses APIs DO return reasoning (thinking blocks with an empty
1072
+ // signature / reasoning items, some with encrypted_content), but probed 2026-09-24 on
1073
+ // fugu-max it isn't worth switching for, since we keep reasoning only to replay it: the
1074
+ // returned text is often a vague status summary or empty rather than the actual
1075
+ // reasoning, and replayed reasoning reaches the model distorted (a planted "4817" came
1076
+ // back as "4829" on both APIs, presumably rewritten by the orchestrator).
1072
1077
  //
1073
1078
  // Base `fugu` was RETIRED 2026-08-04. It was carried as a cheap everyday option at an
1074
1079
  // assumed $1/$3, but reconciling token logs against the Sakana balance showed it actually
@@ -1527,6 +1532,9 @@ function getProviderSignatureFields(aiType, signature) {
1527
1532
  if (aiType.startsWith("grok")) {
1528
1533
  return { grokEncryptedReasoning: signature };
1529
1534
  }
1535
+ if (aiType.startsWith("gpt")) {
1536
+ return { openaiEncryptedReasoning: signature };
1537
+ }
1530
1538
  if (aiType.startsWith("muse-")) {
1531
1539
  return { metaEncryptedReasoning: signature };
1532
1540
  }
@@ -2351,7 +2359,54 @@ ${instruction}`);
2351
2359
  * inherited from a default OpenAI can change under us.
2352
2360
  */
2353
2361
  reasoningParams() {
2354
- return this.reasoningEffort ? { reasoning: { effort: toOpenAIEffort(this.reasoningEffort) } } : {};
2362
+ return {
2363
+ reasoning: {
2364
+ ...this.reasoningEffort ? { effort: toOpenAIEffort(this.reasoningEffort) } : {},
2365
+ // Render reasoning from earlier turns into this sample. It is already the
2366
+ // default on all four models we run, but stated explicitly for the same reason
2367
+ // effort is. It only does anything because we replay the items ourselves.
2368
+ context: "all_turns"
2369
+ }
2370
+ };
2371
+ }
2372
+ /**
2373
+ * Responses API input items, with each assistant turn's stored reasoning items replayed
2374
+ * immediately before it. OpenAI attaches `encrypted_content` to reasoning items whenever
2375
+ * `store` is false, which is how we call it: the documented stateless route is to preserve
2376
+ * the output items and replay the history yourself, rather than lean on
2377
+ * `previous_response_id` and OpenAI-side session state.
2378
+ *
2379
+ * This was a single flattened string before ("User: ... / Assistant: ..."), which left
2380
+ * nowhere to put reasoning items. The system prompt still travels ONLY as `instructions`.
2381
+ *
2382
+ * Reasoning is readable only within one model family and the API silently omits items it
2383
+ * cannot read, so a bot moved between GPT-6 and GPT-5.6 Terra loses its reasoning and keeps
2384
+ * working. A bot moved to another provider never reaches this code at all: every agent
2385
+ * reads only its own field, and a missing one falls back to a plain message.
2386
+ */
2387
+ buildResponsesInput(messages) {
2388
+ const input = [];
2389
+ for (const msg of this.prepareMessages(messages)) {
2390
+ if (msg.role === "assistant" && msg.openaiEncryptedReasoning) {
2391
+ try {
2392
+ const items = JSON.parse(msg.openaiEncryptedReasoning);
2393
+ if (Array.isArray(items)) {
2394
+ input.push(...items);
2395
+ }
2396
+ } catch {
2397
+ this.logger("Failed to parse stored encrypted reasoning, replaying message without it");
2398
+ }
2399
+ }
2400
+ input.push({ role: msg.role, content: msg.content });
2401
+ }
2402
+ return input;
2403
+ }
2404
+ /** The response's reasoning items, JSON-serialized for the caller to store on the message. */
2405
+ extractReasoningItems(response) {
2406
+ const items = (response?.output ?? []).filter(
2407
+ (item) => item?.type === "reasoning" && item.encrypted_content
2408
+ );
2409
+ return items.length > 0 ? JSON.stringify(items) : void 0;
2355
2410
  }
2356
2411
  /**
2357
2412
  * Structured output method using Zod with OpenAI's Responses API
@@ -2363,7 +2418,7 @@ ${instruction}`);
2363
2418
  try {
2364
2419
  this.logAsking(messages);
2365
2420
  this.logMessages(messages);
2366
- const input = this.prepareMessages(messages).map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`).join("\n\n");
2421
+ const input = this.buildResponsesInput(messages);
2367
2422
  const schemaToSend = zodSchema;
2368
2423
  let response;
2369
2424
  try {
@@ -2373,6 +2428,8 @@ ${instruction}`);
2373
2428
  input,
2374
2429
  max_output_tokens: this.maxOutputTokens,
2375
2430
  prompt_cache_key: this.promptCacheKey,
2431
+ // Stateless: encrypted reasoning is only returned for unstored responses.
2432
+ store: false,
2376
2433
  ...this.reasoningParams(),
2377
2434
  text: {
2378
2435
  format: zodTextFormat(schemaToSend, "response_schema")
@@ -2433,7 +2490,7 @@ ${instruction}`);
2433
2490
  this.logReply(response.output_parsed, reasoningContent, tokenUsage);
2434
2491
  }
2435
2492
  this.logger(`\u2705 Response validated successfully with Zod schema`);
2436
- return [response.output_parsed, reasoningContent, tokenUsage];
2493
+ return [response.output_parsed, reasoningContent, tokenUsage, this.extractReasoningItems(response)];
2437
2494
  } catch (error) {
2438
2495
  this.logger(this.logTemplates.error(this.name, error));
2439
2496
  if (error instanceof ModelError) {
@@ -2452,13 +2509,14 @@ ${instruction}`);
2452
2509
  try {
2453
2510
  this.logAsking(messages);
2454
2511
  this.logMessages(messages);
2455
- const input = this.prepareMessages(messages).map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`).join("\n\n");
2512
+ const input = this.buildResponsesInput(messages);
2456
2513
  const response = await this.client.responses.create({
2457
2514
  model: this.model,
2458
2515
  instructions: this.instruction,
2459
2516
  input,
2460
2517
  max_output_tokens: this.maxOutputTokens,
2461
2518
  prompt_cache_key: this.promptCacheKey,
2519
+ store: false,
2462
2520
  ...this.reasoningParams()
2463
2521
  });
2464
2522
  const content = response.output_text;
@@ -2500,7 +2558,7 @@ ${instruction}`);
2500
2558
  }
2501
2559
  }
2502
2560
  this.logReply(content, "", tokenUsage);
2503
- return [content, "", tokenUsage];
2561
+ return [content, "", tokenUsage, this.extractReasoningItems(response)];
2504
2562
  } catch (error) {
2505
2563
  this.logger(this.logTemplates.error(this.name, error));
2506
2564
  if (error instanceof ModelError) {
@@ -4592,13 +4650,29 @@ var QwenAgent = class extends AbstractAgent {
4592
4650
  const budget = this.thinkingBudgetTokens;
4593
4651
  return {
4594
4652
  enable_thinking: this.enableThinking,
4595
- ...this.enableThinking && budget !== void 0 ? { thinking_budget: budget } : {}
4653
+ ...this.enableThinking && budget !== void 0 ? { thinking_budget: budget } : {},
4654
+ // Lets the model read the `reasoning_content` we replay on assistant messages, so a
4655
+ // bot keeps its own train of thought across turns. Qwen documents models as NOT
4656
+ // reading replayed reasoning unless this is set (qwen3.8-max and qwen3.8-flash are
4657
+ // both on the supported list). Measured 2026-09-22: qwen3.8-max actually used the
4658
+ // replayed reasoning with the flag off too, but we follow the documented contract
4659
+ // rather than rely on that. Stateless — the reasoning comes from our payload, not
4660
+ // from any server-side session.
4661
+ ...this.enableThinking ? { preserve_thinking: true } : {}
4596
4662
  };
4597
4663
  }
4664
+ /**
4665
+ * Assistant turns carry their own `reasoning_content` back so the model can reference what
4666
+ * it was thinking on earlier turns (paired with `preserve_thinking` in thinkingParams).
4667
+ * Qwen's reasoning is PLAIN TEXT, not a signed or encrypted blob, so there is nothing for
4668
+ * another provider to reject: a bot whose model changes mid-game simply stops sending it
4669
+ * and keeps working. The text is the same one the caller already stores as `thinking`.
4670
+ */
4598
4671
  convertToOpenAIMessages(messages) {
4599
4672
  return messages.map((msg) => ({
4600
4673
  role: msg.role,
4601
- content: msg.content
4674
+ content: msg.content,
4675
+ ...msg.role === "assistant" && msg.thinking ? { reasoning_content: msg.thinking } : {}
4602
4676
  }));
4603
4677
  }
4604
4678
  extractThinkingAndUsage(completion) {
@@ -4665,7 +4739,11 @@ ${schemaDescription}`;
4665
4739
  const params = {
4666
4740
  ...this.defaultParams,
4667
4741
  messages: openAIMessages,
4668
- ...this.thinkingParams()
4742
+ ...this.thinkingParams(),
4743
+ response_format: {
4744
+ type: "json_schema",
4745
+ json_schema: ZodSchemaConverter.toOpenAIJsonSchema(zodSchema, "response_schema")
4746
+ }
4669
4747
  };
4670
4748
  completion = await this.client.chat.completions.create(params);
4671
4749
  } catch (apiError) {