@hiper2d/ai-agents 0.5.1 → 0.5.2

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.d.mts CHANGED
@@ -272,15 +272,29 @@ declare class ModelInvalidResponseError extends ModelError {
272
272
  constructor(modelType: string, detail: string, truncated?: boolean);
273
273
  }
274
274
  /**
275
- * The model declined to answer: Anthropic returns `stop_reason: "refusal"` with no content
276
- * blocks when its safety layer rejects the request as a whole. Not retryable as-is — the
277
- * same prompt will refuse again — the caller has to change the prompt or the model.
278
- * Observed 2026-08-30 on Claude Fable 5: a persona system prompt plus a narrated multi-turn
279
- * history that ends by asking the character what it does refuses, while either half alone
280
- * answers; Sonnet 5 and Opus 4.8 answer the same requests.
275
+ * The model declined to answer. Not retryable as-is — the same prompt will refuse again —
276
+ * the caller has to change the prompt or the model. `reason` is the provider's own label
277
+ * for the refusal, when it gives one:
278
+ *
279
+ * Anthropic `stop_reason: "refusal"` with no content blocks when its safety layer rejects
280
+ * the request as a whole. Observed 2026-08-30 on Claude Fable 5: a persona
281
+ * system prompt plus a narrated multi-turn history that ends by asking the
282
+ * character what it does refuses, while either half alone answers; Sonnet 5
283
+ * and Opus 4.8 answer the same requests. `reason` = "refusal".
284
+ * Google a 200 with no candidates and `promptFeedback.blockReason` set (the prompt
285
+ * itself was rejected: PROHIBITED_CONTENT, SAFETY, BLOCKLIST, JAILBREAK,
286
+ * MODEL_ARMOR, OTHER), or a candidate whose `finishReason` is a content
287
+ * block (SAFETY, PROHIBITED_CONTENT, BLOCKLIST, RECITATION, SPII,
288
+ * IMAGE_SAFETY, ...). Observed 2026-09-13 in production: an explicit
289
+ * sexual roleplay game was refused by Gemini 3.8 Flash and 3.1 Pro with
290
+ * blockReason PROHIBITED_CONTENT in ~200ms and zero output tokens, while
291
+ * Mistral, MiniMax and Muse answered the same prompt. PROHIBITED_CONTENT is
292
+ * Google's non-configurable filter — no safetySettings change lifts it.
293
+ * `reason` = the blockReason or finishReason string.
281
294
  */
282
295
  declare class ModelRefusalError extends ModelError {
283
- constructor(modelType: string, message?: string);
296
+ reason?: string;
297
+ constructor(modelType: string, message?: string, reason?: string);
284
298
  }
285
299
 
286
300
  /**
package/dist/index.d.ts CHANGED
@@ -272,15 +272,29 @@ declare class ModelInvalidResponseError extends ModelError {
272
272
  constructor(modelType: string, detail: string, truncated?: boolean);
273
273
  }
274
274
  /**
275
- * The model declined to answer: Anthropic returns `stop_reason: "refusal"` with no content
276
- * blocks when its safety layer rejects the request as a whole. Not retryable as-is — the
277
- * same prompt will refuse again — the caller has to change the prompt or the model.
278
- * Observed 2026-08-30 on Claude Fable 5: a persona system prompt plus a narrated multi-turn
279
- * history that ends by asking the character what it does refuses, while either half alone
280
- * answers; Sonnet 5 and Opus 4.8 answer the same requests.
275
+ * The model declined to answer. Not retryable as-is — the same prompt will refuse again —
276
+ * the caller has to change the prompt or the model. `reason` is the provider's own label
277
+ * for the refusal, when it gives one:
278
+ *
279
+ * Anthropic `stop_reason: "refusal"` with no content blocks when its safety layer rejects
280
+ * the request as a whole. Observed 2026-08-30 on Claude Fable 5: a persona
281
+ * system prompt plus a narrated multi-turn history that ends by asking the
282
+ * character what it does refuses, while either half alone answers; Sonnet 5
283
+ * and Opus 4.8 answer the same requests. `reason` = "refusal".
284
+ * Google a 200 with no candidates and `promptFeedback.blockReason` set (the prompt
285
+ * itself was rejected: PROHIBITED_CONTENT, SAFETY, BLOCKLIST, JAILBREAK,
286
+ * MODEL_ARMOR, OTHER), or a candidate whose `finishReason` is a content
287
+ * block (SAFETY, PROHIBITED_CONTENT, BLOCKLIST, RECITATION, SPII,
288
+ * IMAGE_SAFETY, ...). Observed 2026-09-13 in production: an explicit
289
+ * sexual roleplay game was refused by Gemini 3.8 Flash and 3.1 Pro with
290
+ * blockReason PROHIBITED_CONTENT in ~200ms and zero output tokens, while
291
+ * Mistral, MiniMax and Muse answered the same prompt. PROHIBITED_CONTENT is
292
+ * Google's non-configurable filter — no safetySettings change lifts it.
293
+ * `reason` = the blockReason or finishReason string.
281
294
  */
282
295
  declare class ModelRefusalError extends ModelError {
283
- constructor(modelType: string, message?: string);
296
+ reason?: string;
297
+ constructor(modelType: string, message?: string, reason?: string);
284
298
  }
285
299
 
286
300
  /**
package/dist/index.js CHANGED
@@ -883,9 +883,11 @@ var ModelInvalidResponseError = class extends ModelError {
883
883
  }
884
884
  };
885
885
  var ModelRefusalError = class extends ModelError {
886
- constructor(modelType, message = `${modelType} refused to answer (stop_reason: refusal)`) {
886
+ reason;
887
+ constructor(modelType, message = `${modelType} refused to answer (stop_reason: refusal)`, reason) {
887
888
  super(message, modelType);
888
889
  this.name = "ModelRefusalError";
890
+ this.reason = reason;
889
891
  }
890
892
  };
891
893
 
@@ -2823,7 +2825,7 @@ ${schemaDescription}`;
2823
2825
  throw new Error(this.errorMessages.apiError(apiError));
2824
2826
  }
2825
2827
  if (response.stop_reason === "refusal") {
2826
- throw new ModelRefusalError(this.model);
2828
+ throw new ModelRefusalError(this.model, void 0, "refusal");
2827
2829
  }
2828
2830
  if (!("content" in response) || !Array.isArray(response.content) || response.content.length === 0) {
2829
2831
  throw new Error(this.errorMessages.emptyResponse);
@@ -2917,7 +2919,7 @@ ${schemaDescription}`;
2917
2919
  throw new Error(this.errorMessages.apiError(apiError));
2918
2920
  }
2919
2921
  if (response.stop_reason === "refusal") {
2920
- throw new ModelRefusalError(this.model);
2922
+ throw new ModelRefusalError(this.model, void 0, "refusal");
2921
2923
  }
2922
2924
  if (!("content" in response) || !Array.isArray(response.content) || response.content.length === 0) {
2923
2925
  throw new Error(this.errorMessages.emptyResponse);
@@ -2973,6 +2975,45 @@ ${schemaDescription}`;
2973
2975
 
2974
2976
  // src/agents/google-agent.ts
2975
2977
  var import_genai3 = require("@google/genai");
2978
+ var GEMINI_REFUSAL_FINISH_REASONS = /* @__PURE__ */ new Set([
2979
+ "SAFETY",
2980
+ "RECITATION",
2981
+ "BLOCKLIST",
2982
+ "PROHIBITED_CONTENT",
2983
+ "SPII",
2984
+ "IMAGE_SAFETY",
2985
+ "IMAGE_PROHIBITED_CONTENT",
2986
+ "IMAGE_RECITATION"
2987
+ ]);
2988
+ function throwForEmptyGeminiResponse(model, response, fallback) {
2989
+ const blockReason = response?.promptFeedback?.blockReason;
2990
+ const candidate = response?.candidates?.[0];
2991
+ const finishReason = candidate?.finishReason;
2992
+ const finishMessage = candidate?.finishMessage;
2993
+ const ratings = (candidate?.safetyRatings ?? response?.promptFeedback?.safetyRatings ?? []).filter((r) => r?.blocked || r?.probability && r.probability !== "NEGLIGIBLE");
2994
+ const ratingText = ratings.length ? `; safetyRatings: ${ratings.map((r) => `${r.category}=${r.probability ?? (r.blocked ? "blocked" : "?")}`).join(", ")}` : "";
2995
+ if (blockReason) {
2996
+ throw new ModelRefusalError(
2997
+ model,
2998
+ `${model} refused the prompt (blockReason: ${blockReason}${finishMessage ? `, ${finishMessage}` : ""}${ratingText})`,
2999
+ blockReason
3000
+ );
3001
+ }
3002
+ if (finishReason && GEMINI_REFUSAL_FINISH_REASONS.has(finishReason)) {
3003
+ throw new ModelRefusalError(
3004
+ model,
3005
+ `${model} refused to answer (finishReason: ${finishReason}${finishMessage ? `, ${finishMessage}` : ""}${ratingText})`,
3006
+ finishReason
3007
+ );
3008
+ }
3009
+ if (finishReason === "MAX_TOKENS") {
3010
+ throw new ModelInvalidResponseError(model, "empty response, finishReason MAX_TOKENS (thinking consumed the whole output budget)", true);
3011
+ }
3012
+ const bits = [`finishReason=${finishReason ?? "none"}`];
3013
+ if (!response?.candidates?.length) bits.push("no candidates");
3014
+ if (finishMessage) bits.push(`finishMessage="${finishMessage}"`);
3015
+ throw new Error(`${fallback} (${bits.join(", ")})`);
3016
+ }
2976
3017
  var GoogleAgent = class extends AbstractAgent {
2977
3018
  client;
2978
3019
  defaultConfig = {
@@ -2984,7 +3025,7 @@ var GoogleAgent = class extends AbstractAgent {
2984
3025
  };
2985
3026
  // Error message templates
2986
3027
  errorMessages = {
2987
- emptyResponse: "Empty response from Google API - check logs for detailed response info",
3028
+ emptyResponse: "Empty response from Google API",
2988
3029
  invalidFormat: "Invalid response format from Google API",
2989
3030
  apiError: (error) => `Failed to get response from Google API: ${error instanceof Error ? error.message : String(error)}`,
2990
3031
  unsupportedRole: (role) => `Unsupported role type: ${role}`
@@ -3156,7 +3197,7 @@ var GoogleAgent = class extends AbstractAgent {
3156
3197
  }
3157
3198
  this.logger(`Zod schema response received - hasText: ${!!response.text}, textLength: ${response.text ? response.text.length : 0}`);
3158
3199
  if (!response.text) {
3159
- throw new Error(this.errorMessages.emptyResponse);
3200
+ throwForEmptyGeminiResponse(this.model, response, this.errorMessages.emptyResponse);
3160
3201
  }
3161
3202
  const parsedData = parseAndValidateLlmJson(response.text, zodSchema, (m) => this.logger(m));
3162
3203
  if (parsedData) {
@@ -3244,7 +3285,7 @@ var GoogleAgent = class extends AbstractAgent {
3244
3285
  }
3245
3286
  this.logger(`Plain text response received - hasText: ${!!response.text}, textLength: ${response.text ? response.text.length : 0}`);
3246
3287
  if (!response.text) {
3247
- throw new Error(this.errorMessages.emptyResponse);
3288
+ throwForEmptyGeminiResponse(this.model, response, this.errorMessages.emptyResponse);
3248
3289
  }
3249
3290
  this.logReply(response.text, thinkingContent || void 0, tokenUsage);
3250
3291
  return [response.text, thinkingContent, tokenUsage, googleThoughtSignature || void 0];