feat(catalog): added capability flags and thinking support for Claude Sonnet 5
- Added an `isAnthropicAdaptiveGenAtLeast` utility to classify adaptive-thinking Claude generations at or above a given version threshold. - Enabled adaptive thinking display, sampling restrictions, and mid-conversation system message flags for Claude Sonnet 5+ models. - Updated Bedrock and OpenRouter adaptive reasoning effort maps to support five-tier scales on Sonnet 5+ models.
This commit is contained in:
@@ -2,6 +2,17 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added adaptive thinking display, API restrictions, and mid-conversation system message support for Claude Sonnet 5
|
||||
- Extended 5-tier adaptive reasoning effort map support on OpenRouter and direct APIs to Claude Sonnet 5 and higher
|
||||
|
||||
- Added compatibility flags and routines for Claude Sonnet 5, aligning its capabilities with Claude Opus 4.8 and Fable 5. Specifically:
|
||||
- Enabled adaptive thinking display support (`supportsAdaptiveThinkingDisplay`).
|
||||
- Applied sampling parameter and default thinking omission API restrictions (`hasOpus47ApiRestrictions`).
|
||||
- Added mid-conversation system message support (`supportsMidConversationSystemMessages`).
|
||||
- Extended the 5-tier adaptive reasoning effort map (including real `xhigh` and `max` levels) and Bedrock Converse/OpenRouter adaptive reasoning support.
|
||||
|
||||
## [16.2.7] - 2026-06-30
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -158,6 +158,20 @@ export function isFableOrMythos(kind: AnthropicKind): boolean {
|
||||
return kind === "fable" || kind === "mythos";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the parsed Anthropic model is part of the adaptive-thinking
|
||||
* Claude generation at or above a specific capability threshold.
|
||||
* - Opus has a configurable minimum version floor (e.g. "4.6", "4.7", "4.8").
|
||||
* - Sonnet, Fable, and Mythos all require version 5 or higher.
|
||||
*/
|
||||
export function isAnthropicAdaptiveGenAtLeast(parsed: AnthropicModel, opusMin: "4.6" | "4.7" | "4.8"): boolean {
|
||||
if (parsed.kind === "opus") {
|
||||
return semverGte(parsed.version, opusMin);
|
||||
}
|
||||
// Sonnet 5+, Fable 5+, Mythos 5+, and any future gen-5+ models
|
||||
return semverGte(parsed.version, "5");
|
||||
}
|
||||
|
||||
function createSemVer(major: number, minor: number, patch = 0): SemVer {
|
||||
return { major, minor, patch };
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import {
|
||||
bareModelId,
|
||||
isAnthropicAdaptiveGenAtLeast,
|
||||
isFableOrMythos,
|
||||
parseAnthropicModel,
|
||||
parseGlmModel,
|
||||
@@ -201,41 +202,38 @@ export const modelFamilyToken = memo((modelId: string): string => {
|
||||
});
|
||||
|
||||
/**
|
||||
* Adaptive thinking `display` is supported starting with Claude Opus 4.7 and
|
||||
* the Claude Fable/Mythos 5 generation. Older adaptive-thinking models
|
||||
* (Opus 4.6, Sonnet 4.6+) reject the field. Classifier-based, so dotted and
|
||||
* dashed version forms both match while bare dated ids
|
||||
* Adaptive thinking `display` is supported starting with Claude Opus 4.7+,
|
||||
* Sonnet 5+, and the Claude Fable/Mythos 5 generation. Older adaptive-thinking
|
||||
* models (Opus 4.6, Sonnet 4.6) reject the field. Classifier-based, so dotted
|
||||
* and dashed version forms both match while bare dated ids
|
||||
* (`claude-opus-4-20250514` = Opus 4.0) stay excluded.
|
||||
*/
|
||||
export const supportsAdaptiveThinkingDisplay = memo((modelId: string): boolean => {
|
||||
const parsed = parseAnthropicModel(bareModelId(modelId));
|
||||
if (!parsed) return false;
|
||||
if (isFableOrMythos(parsed.kind)) return semverGte(parsed.version, "5");
|
||||
return parsed.kind === "opus" && semverGte(parsed.version, "4.7");
|
||||
return parsed !== null && isAnthropicAdaptiveGenAtLeast(parsed, "4.7");
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns true for Anthropic models with Opus 4.7+/Fable/Mythos API restrictions:
|
||||
* Returns true for Anthropic models with Opus 4.7+, Sonnet 5+, and Fable/Mythos 5+
|
||||
* API restrictions:
|
||||
* - Sampling parameters (temperature/top_p/top_k) return 400 error
|
||||
* - Thinking content is omitted by default (needs display: "summarized")
|
||||
*/
|
||||
export const hasOpus47ApiRestrictions = memo((modelId: string): boolean => {
|
||||
const parsed = parseAnthropicModel(bareModelId(modelId));
|
||||
if (!parsed) return false;
|
||||
return (parsed.kind === "opus" && semverGte(parsed.version, "4.7")) || isFableOrMythos(parsed.kind);
|
||||
return parsed !== null && isAnthropicAdaptiveGenAtLeast(parsed, "4.7");
|
||||
});
|
||||
|
||||
/**
|
||||
* Mid-conversation `role: "system"` messages (system instructions appended at
|
||||
* non-first positions in the `messages` array) are supported starting with
|
||||
* Claude Opus 4.8 and the Claude Fable/Mythos 5 generation. Earlier Claude
|
||||
* models reject the role.
|
||||
* Claude Opus 4.8+, Sonnet 5+, and the Claude Fable/Mythos 5 generation.
|
||||
* Earlier Claude models reject the role.
|
||||
* @see https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages
|
||||
*/
|
||||
export const supportsMidConversationSystemMessages = memo((modelId: string): boolean => {
|
||||
const parsed = parseAnthropicModel(bareModelId(modelId));
|
||||
if (!parsed) return false;
|
||||
return (parsed.kind === "opus" && semverGte(parsed.version, "4.8")) || isFableOrMythos(parsed.kind);
|
||||
return parsed !== null && isAnthropicAdaptiveGenAtLeast(parsed, "4.8");
|
||||
});
|
||||
|
||||
export const isAnthropicFableOrMythosModel = memo((modelId: string): boolean => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
type AnthropicModel,
|
||||
bareModelId,
|
||||
type GeminiModel,
|
||||
isFableOrMythos,
|
||||
isAnthropicAdaptiveGenAtLeast,
|
||||
type OpenAIModel,
|
||||
type ParsedModel,
|
||||
parseAnthropicModel,
|
||||
@@ -441,12 +441,11 @@ function isDeepseekReasoningModel<TApi extends Api>(spec: ModelSpec<TApi>): bool
|
||||
function getOpenRouterAnthropicReasoningEffortMap(modelId: string): EffortMap | undefined {
|
||||
const parsed = parseAnthropicModel(bareModelId(modelId));
|
||||
if (!parsed) return undefined;
|
||||
// Adaptive efforts on OpenRouter's completions front: Fable/Mythos and
|
||||
// Opus 4.6+ only — Sonnet stays on the plain effort vocabulary there.
|
||||
const isOpusAdaptive = parsed.kind === "opus" && semverGte(parsed.version, "4.6");
|
||||
if (!isFableOrMythos(parsed.kind) && !isOpusAdaptive) return undefined;
|
||||
// Adaptive efforts on OpenRouter's completions front: Fable/Mythos, Sonnet 5+,
|
||||
// and Opus 4.6+ only — older Sonnet versions stay on the plain effort vocabulary there.
|
||||
if (!isAnthropicAdaptiveGenAtLeast(parsed, "4.6")) return undefined;
|
||||
|
||||
const hasRealXHigh = isFableOrMythos(parsed.kind) || semverGte(parsed.version, "4.7");
|
||||
const hasRealXHigh = isAnthropicAdaptiveGenAtLeast(parsed, "4.7");
|
||||
return hasRealXHigh ? ANTHROPIC_ADAPTIVE_EFFORT_MAP_5_TIER : ANTHROPIC_ADAPTIVE_EFFORT_MAP_4_TIER;
|
||||
}
|
||||
|
||||
@@ -521,7 +520,7 @@ function inferAnthropicSupportedEfforts<TApi extends Api>(
|
||||
(spec.api === "anthropic-messages" || spec.api === "bedrock-converse-stream") &&
|
||||
semverGte(parsedModel.version, "4.6")
|
||||
) {
|
||||
return parsedModel.kind === "opus" || isFableOrMythos(parsedModel.kind)
|
||||
return isAnthropicAdaptiveGenAtLeast(parsedModel, "4.6")
|
||||
? DEFAULT_REASONING_EFFORTS_WITH_XHIGH
|
||||
: DEFAULT_REASONING_EFFORTS;
|
||||
}
|
||||
@@ -601,10 +600,7 @@ function inferThinkingControlMode<TApi extends Api>(
|
||||
|
||||
case "bedrock-converse-stream":
|
||||
if (parsedModel.family === "anthropic") {
|
||||
if (
|
||||
semverGte(parsedModel.version, "4.6") &&
|
||||
(parsedModel.kind === "opus" || isFableOrMythos(parsedModel.kind))
|
||||
) {
|
||||
if (isAnthropicAdaptiveGenAtLeast(parsedModel, "4.6")) {
|
||||
return "anthropic-adaptive";
|
||||
}
|
||||
// Opus 4.5 on Bedrock metadata mirrors the direct-Anthropic
|
||||
@@ -627,19 +623,18 @@ function isOpenRouterAnthropicAdaptiveReasoningModel<TApi extends Api>(
|
||||
): boolean {
|
||||
if (!isOpenAICompatReasoningApi(spec.api)) return false;
|
||||
if (!modelMatchesHost(spec, "openrouter")) return false;
|
||||
return isFableOrMythos(parsedModel.kind) || (parsedModel.kind === "opus" && semverGte(parsedModel.version, "4.6"));
|
||||
return isAnthropicAdaptiveGenAtLeast(parsedModel, "4.6");
|
||||
}
|
||||
|
||||
/**
|
||||
* Opus 4.7+ and Fable/Mythos on the Messages API expose the full five-tier
|
||||
* Opus 4.7+, Sonnet 5+, and Fable/Mythos 5+ on the Messages API expose the full five-tier
|
||||
* adaptive scale (low/medium/high/xhigh/max). Bedrock Converse stays on the
|
||||
* four-tier scale regardless of model version.
|
||||
*/
|
||||
function anthropicModelHasRealXHighEffort<TApi extends Api>(spec: ModelSpec<TApi>, parsedModel: ParsedModel): boolean {
|
||||
if (spec.api !== "anthropic-messages") return false;
|
||||
if (parsedModel.family !== "anthropic") return false;
|
||||
if (isFableOrMythos(parsedModel.kind)) return true;
|
||||
return parsedModel.kind === "opus" && semverGte(parsedModel.version, "4.7");
|
||||
return isAnthropicAdaptiveGenAtLeast(parsedModel, "4.7");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
hasOpus47ApiRestrictions,
|
||||
isClaudeModelId,
|
||||
isGlmVisionModelId,
|
||||
isGrokReasoningEffortCapable,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
isReasoningGlmModelId,
|
||||
modelFamilyToken,
|
||||
supportsAdaptiveThinkingDisplay,
|
||||
supportsMidConversationSystemMessages,
|
||||
} from "@oh-my-pi/pi-catalog/identity";
|
||||
|
||||
describe("isKimiModelId", () => {
|
||||
@@ -44,10 +46,12 @@ describe("isClaudeModelId", () => {
|
||||
});
|
||||
|
||||
describe("supportsAdaptiveThinkingDisplay", () => {
|
||||
test("allows Claude Fable 5 and Opus 4.7 or newer only", () => {
|
||||
test("allows Claude Fable 5, Opus 4.7 or newer, and Sonnet 5 or newer only", () => {
|
||||
expect(supportsAdaptiveThinkingDisplay("claude-fable-5")).toBe(true);
|
||||
expect(supportsAdaptiveThinkingDisplay("claude-opus-4-7")).toBe(true);
|
||||
expect(supportsAdaptiveThinkingDisplay("claude-opus-5-0")).toBe(true);
|
||||
expect(supportsAdaptiveThinkingDisplay("claude-sonnet-5")).toBe(true);
|
||||
expect(supportsAdaptiveThinkingDisplay("us.anthropic.claude-sonnet-5")).toBe(true);
|
||||
// Dotted and dashed version separators are equivalent.
|
||||
expect(supportsAdaptiveThinkingDisplay("claude-opus-4.7")).toBe(true);
|
||||
expect(supportsAdaptiveThinkingDisplay("anthropic/claude-opus-4.8")).toBe(true);
|
||||
@@ -58,6 +62,30 @@ describe("supportsAdaptiveThinkingDisplay", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasOpus47ApiRestrictions", () => {
|
||||
test("allows Claude Fable 5, Opus 4.7 or newer, and Sonnet 5 or newer only", () => {
|
||||
expect(hasOpus47ApiRestrictions("claude-fable-5")).toBe(true);
|
||||
expect(hasOpus47ApiRestrictions("claude-opus-4-7")).toBe(true);
|
||||
expect(hasOpus47ApiRestrictions("claude-opus-4.8")).toBe(true);
|
||||
expect(hasOpus47ApiRestrictions("claude-sonnet-5")).toBe(true);
|
||||
expect(hasOpus47ApiRestrictions("us.anthropic.claude-sonnet-5")).toBe(true);
|
||||
expect(hasOpus47ApiRestrictions("claude-opus-4-6")).toBe(false);
|
||||
expect(hasOpus47ApiRestrictions("claude-sonnet-4-6")).toBe(false);
|
||||
expect(hasOpus47ApiRestrictions("claude-sonnet-4-5")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("supportsMidConversationSystemMessages", () => {
|
||||
test("allows Claude Fable 5, Opus 4.8 or newer, and Sonnet 5 or newer only", () => {
|
||||
expect(supportsMidConversationSystemMessages("claude-fable-5")).toBe(true);
|
||||
expect(supportsMidConversationSystemMessages("claude-opus-4-8")).toBe(true);
|
||||
expect(supportsMidConversationSystemMessages("claude-sonnet-5")).toBe(true);
|
||||
expect(supportsMidConversationSystemMessages("us.anthropic.claude-sonnet-5")).toBe(true);
|
||||
expect(supportsMidConversationSystemMessages("claude-opus-4-7")).toBe(false);
|
||||
expect(supportsMidConversationSystemMessages("claude-sonnet-4-6")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isMinimaxM2FamilyModelId", () => {
|
||||
test("matches every M2-generation id shape served by aggregator/native hosts", () => {
|
||||
// Fireworks/OpenCode/openrouter direct ids and `-highspeed`/`-lightning` variants.
|
||||
|
||||
@@ -367,6 +367,12 @@ describe("model thinking derivation", () => {
|
||||
provider: "amazon-bedrock",
|
||||
});
|
||||
const sonnet46 = createModel({ id: "claude-sonnet-4.6", api: "anthropic-messages", provider: "anthropic" });
|
||||
const sonnet5 = createModel({ id: "claude-sonnet-5", api: "anthropic-messages", provider: "anthropic" });
|
||||
const sonnet5Bedrock = createModel({
|
||||
id: "global.anthropic.claude-sonnet-5",
|
||||
api: "bedrock-converse-stream",
|
||||
provider: "amazon-bedrock",
|
||||
});
|
||||
const mythos = createModel({ id: "claude-mythos-5", api: "anthropic-messages", provider: "anthropic" });
|
||||
const mythosBedrock = createModel({
|
||||
id: "global.anthropic.claude-mythos-5",
|
||||
@@ -399,6 +405,8 @@ describe("model thinking derivation", () => {
|
||||
expect(sonnet45Bedrock.thinking?.mode).toBe("budget");
|
||||
expect(opus46.thinking?.mode).toBe("anthropic-adaptive");
|
||||
expect(sonnet46.thinking?.mode).toBe("anthropic-adaptive");
|
||||
expect(sonnet5.thinking?.mode).toBe("anthropic-adaptive");
|
||||
expect(sonnet5Bedrock.thinking?.mode).toBe("anthropic-adaptive");
|
||||
expect(mythosBedrock.thinking?.mode).toBe("anthropic-adaptive");
|
||||
expect(minimaxM2.thinking).toEqual({
|
||||
mode: "anthropic-adaptive",
|
||||
@@ -437,13 +445,16 @@ describe("model thinking derivation", () => {
|
||||
expect(mapEffortToAnthropicAdaptiveEffort(opus47, Effort.XHigh)).toBe("max");
|
||||
expect(mapEffortToAnthropicAdaptiveEffort(mythos, Effort.High)).toBe("xhigh");
|
||||
expect(mapEffortToAnthropicAdaptiveEffort(mythosBedrock, Effort.XHigh)).toBe("max");
|
||||
expect(mapEffortToAnthropicAdaptiveEffort(sonnet5, Effort.High)).toBe("xhigh");
|
||||
expect(mapEffortToAnthropicAdaptiveEffort(sonnet5Bedrock, Effort.XHigh)).toBe("max");
|
||||
// Bedrock Converse keeps the four-tier legacy mapping; xhigh aliases to "max".
|
||||
expect(opus47Bedrock.thinking?.effortMap).toEqual({ minimal: "low", xhigh: "max" });
|
||||
expect(mapEffortToAnthropicAdaptiveEffort(opus47Bedrock, Effort.High)).toBe("high");
|
||||
expect(mapEffortToAnthropicAdaptiveEffort(sonnet5Bedrock, Effort.High)).toBe("high");
|
||||
expect(() => mapEffortToAnthropicAdaptiveEffort(sonnet46, Effort.XHigh)).toThrow(/not supported/);
|
||||
});
|
||||
|
||||
it("bakes adaptive display support for Opus 4.7+ and Fable/Mythos 5", () => {
|
||||
it("bakes adaptive display support for Opus 4.7+, Sonnet 5+, and Fable/Mythos 5", () => {
|
||||
const opus46 = createModel({ id: "claude-opus-4.6", api: "anthropic-messages", provider: "anthropic" });
|
||||
const opus47 = createModel({ id: "claude-opus-4-7", api: "anthropic-messages", provider: "anthropic" });
|
||||
// Dotted and dashed version forms are equivalent; bare dated ids stay Opus 4.0.
|
||||
@@ -459,6 +470,12 @@ describe("model thinking derivation", () => {
|
||||
api: "bedrock-converse-stream",
|
||||
provider: "amazon-bedrock",
|
||||
});
|
||||
const sonnet5 = createModel({ id: "claude-sonnet-5", api: "anthropic-messages", provider: "anthropic" });
|
||||
const sonnet5Bedrock = createModel({
|
||||
id: "global.anthropic.claude-sonnet-5",
|
||||
api: "bedrock-converse-stream",
|
||||
provider: "amazon-bedrock",
|
||||
});
|
||||
|
||||
expect(opus46.thinking?.supportsDisplay).toBeUndefined();
|
||||
expect(opus47.thinking?.supportsDisplay).toBe(true);
|
||||
@@ -466,6 +483,8 @@ describe("model thinking derivation", () => {
|
||||
expect(opus4Dated.thinking?.supportsDisplay).toBeUndefined();
|
||||
expect(fable.thinking?.supportsDisplay).toBe(true);
|
||||
expect(fableBedrock.thinking?.supportsDisplay).toBe(true);
|
||||
expect(sonnet5.thinking?.supportsDisplay).toBe(true);
|
||||
expect(sonnet5Bedrock.thinking?.supportsDisplay).toBe(true);
|
||||
});
|
||||
|
||||
it("backfills wire facts onto explicit thinking, explicit values winning", () => {
|
||||
@@ -526,10 +545,12 @@ describe("model thinking derivation", () => {
|
||||
it("bakes sampling-param rejection into anthropic compat", () => {
|
||||
const sonnet45 = createModel({ id: "claude-sonnet-4-5", api: "anthropic-messages", provider: "anthropic" });
|
||||
const opus47 = createModel({ id: "claude-opus-4.7", api: "anthropic-messages", provider: "anthropic" });
|
||||
const sonnet5 = createModel({ id: "claude-sonnet-5", api: "anthropic-messages", provider: "anthropic" });
|
||||
const fable = createModel({ id: "claude-fable-5", api: "anthropic-messages", provider: "anthropic" });
|
||||
|
||||
expect(sonnet45.compat.supportsSamplingParams).toBe(true);
|
||||
expect(opus47.compat.supportsSamplingParams).toBe(false);
|
||||
expect(sonnet5.compat.supportsSamplingParams).toBe(false);
|
||||
expect(fable.compat.supportsSamplingParams).toBe(false);
|
||||
});
|
||||
|
||||
@@ -685,11 +706,17 @@ describe("model thinking runtime helpers", () => {
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
});
|
||||
|
||||
const sonnet5 = createModel({
|
||||
id: "anthropic/claude-sonnet-5",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
});
|
||||
expect(fable.thinking?.efforts.at(-1)).toBe(Effort.XHigh);
|
||||
expect(opus46.thinking?.efforts.at(-1)).toBe(Effort.XHigh);
|
||||
expect(sonnet46.thinking?.efforts.at(-1)).toBe(Effort.High);
|
||||
expect(sonnet5.thinking?.efforts.at(-1)).toBe(Effort.XHigh);
|
||||
expect(requireSupportedEffort(fable, Effort.XHigh)).toBe(Effort.XHigh);
|
||||
expect(requireSupportedEffort(sonnet5, Effort.XHigh)).toBe(Effort.XHigh);
|
||||
});
|
||||
|
||||
it("enables xhigh for openai-responses and openai-codex-responses APIs", () => {
|
||||
|
||||
@@ -310,4 +310,68 @@ describe("AgentSession thinking-loop retry", () => {
|
||||
expect(assistants).toHaveLength(1);
|
||||
expect(assistants[0].content).toEqual([{ type: "text", text: "Recovered after retry." }]);
|
||||
});
|
||||
|
||||
it("injects a redirect notice on each consecutive thinking-loop retry", async () => {
|
||||
const model = createMockModel({ provider: "openrouter", id: "google/gemini-3.5-flash" }).model;
|
||||
const modelRegistry = new ModelRegistry(authStorage);
|
||||
const calls: string[] = [];
|
||||
const contexts: Context[] = [];
|
||||
const agent = new Agent({
|
||||
getApiKey: requestedModel => `${requestedModel.provider}-test-key`,
|
||||
initialState: {
|
||||
model,
|
||||
systemPrompt: ["Test"],
|
||||
tools: [],
|
||||
messages: [],
|
||||
},
|
||||
convertToLlm,
|
||||
streamFn: (requestedModel, context, _options?: SimpleStreamOptions) => {
|
||||
calls.push(`${requestedModel.provider}/${requestedModel.id}`);
|
||||
contexts.push(context);
|
||||
return calls.length <= 2 ? errorIdOnlyThinkingLoopStream(requestedModel) : successStream(requestedModel);
|
||||
},
|
||||
});
|
||||
const settings = Settings.isolated({
|
||||
"compaction.enabled": false,
|
||||
"retry.enabled": true,
|
||||
"retry.baseDelayMs": 0,
|
||||
"retry.maxDelayMs": 5_000,
|
||||
"retry.maxRetries": 2,
|
||||
"retry.modelFallback": false,
|
||||
"todo.enabled": false,
|
||||
"model.loopGuard.enabled": true,
|
||||
});
|
||||
settings.setModelRole("default", `${model.provider}/${model.id}`);
|
||||
session = new AgentSession({
|
||||
agent,
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
settings,
|
||||
modelRegistry,
|
||||
});
|
||||
vi.spyOn(scheduler, "wait").mockResolvedValue(undefined);
|
||||
|
||||
await session.prompt("Trigger redirect injection after two thinking loops");
|
||||
await session.waitForIdle();
|
||||
|
||||
expect(calls).toHaveLength(3);
|
||||
const redirects = session.agent.state.messages.filter(
|
||||
(message): message is CustomMessage =>
|
||||
message.role === "custom" && message.customType === "thinking-loop-redirect",
|
||||
);
|
||||
expect(redirects).toHaveLength(2);
|
||||
const extractText = (content: string | Array<{ type: string; text?: string }>): string =>
|
||||
typeof content === "string"
|
||||
? content
|
||||
: content.map(part => (part.type === "text" ? (part.text ?? "") : "")).join("");
|
||||
const thirdAttemptRedirectDevMsgs = contexts[2].messages.filter(
|
||||
message => message.role === "developer" && extractText(message.content).includes("thinking_loop_detected"),
|
||||
);
|
||||
expect(thirdAttemptRedirectDevMsgs).toHaveLength(2);
|
||||
|
||||
const assistants = session.agent.state.messages.filter(
|
||||
(message): message is AssistantMessage => message.role === "assistant",
|
||||
);
|
||||
expect(assistants).toHaveLength(1);
|
||||
expect(assistants[0].content).toEqual([{ type: "text", text: "Recovered after retry." }]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user