mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
improve(gateway): compose live session stress probes (#122519)
* test(gateway): compose live session stress probes Amp-Thread-ID: https://ampcode.com/threads/T-019feaaa-c7ed-769e-9f29-a3612bec72e7 * fix(ai): resume after Responses compaction checkpoints Amp-Thread-ID: https://ampcode.com/threads/T-019feaaa-c7ed-769e-9f29-a3612bec72e7 * test(gateway): compose multi-session subagent probes Amp-Thread-ID: https://ampcode.com/threads/T-019feaaa-c7ed-769e-9f29-a3612bec72e7 * fix(test): invalidate inlined schema transforms Amp-Thread-ID: https://ampcode.com/threads/T-019feaaa-c7ed-769e-9f29-a3612bec72e7 * test(ai): cover empty compaction owners Amp-Thread-ID: https://ampcode.com/threads/T-019feaaa-c7ed-769e-9f29-a3612bec72e7 --------- Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
committed by
GitHub
parent
9b90e104c5
commit
2cb9a75648
@@ -77,9 +77,9 @@ Implementation:
|
|||||||
- Max image side is configurable via `agents.defaults.imageMaxDimensionPx`
|
- Max image side is configurable via `agents.defaults.imageMaxDimensionPx`
|
||||||
(default: `1200`)
|
(default: `1200`)
|
||||||
- Blank text blocks are removed while this pass walks replay content.
|
- Blank text blocks are removed while this pass walks replay content.
|
||||||
Assistant turns that become empty are dropped from the replay copy; user
|
Assistant turns that become empty are dropped unless they own opaque
|
||||||
and tool-result turns that become empty receive a non-empty
|
provider replay state; user and tool-result turns that become empty receive
|
||||||
omitted-content placeholder.
|
a non-empty omitted-content placeholder.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -928,19 +928,15 @@ describe("OpenAI Responses compaction replay", () => {
|
|||||||
expect(input.map((item) => item.type)).toEqual(["compaction", "message"]);
|
expect(input.map((item) => item.type)).toEqual(["compaction", "message"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("replays when session and auth identities match", () => {
|
it.each(responseConverters)(
|
||||||
const assistant = createOutput();
|
"$name replays an empty checkpoint owner when request identities match",
|
||||||
assistant.providerReplay = compactionState(model, { replayIndex: 0 });
|
({ convert }) => {
|
||||||
|
const assistant = createOutput();
|
||||||
|
assistant.providerReplay = compactionState(model, { replayIndex: 0 });
|
||||||
|
|
||||||
const input = convertResponsesMessages(
|
expect(convert({ messages: [assistant] }).map((item) => item.type)).toEqual(["compaction"]);
|
||||||
model,
|
},
|
||||||
{ messages: [assistant] },
|
);
|
||||||
new Set(["openai"]),
|
|
||||||
replayIdentity,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(input.some((item) => item.type === "compaction")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each(responseConverters)(
|
it.each(responseConverters)(
|
||||||
"$name does not replay or prune across a different or missing request identity",
|
"$name does not replay or prune across a different or missing request identity",
|
||||||
|
|||||||
@@ -21,9 +21,18 @@ export function createStateSchemaInlinePlugin(rootDir = process.cwd()) {
|
|||||||
const schemasByModulePath = new Map(
|
const schemasByModulePath = new Map(
|
||||||
STATE_SCHEMA_MODULES.map((schema) => [path.resolve(rootDir, schema.modulePath), schema]),
|
STATE_SCHEMA_MODULES.map((schema) => [path.resolve(rootDir, schema.modulePath), schema]),
|
||||||
);
|
);
|
||||||
|
const cacheKeyForSchema = ({ id }: { id: string }) => {
|
||||||
|
const schema = schemasByModulePath.get(path.resolve(id));
|
||||||
|
return schema ? fs.readFileSync(path.resolve(rootDir, schema.schemaPath), "utf8") : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: STATE_SCHEMA_INLINE_PLUGIN_NAME,
|
name: STATE_SCHEMA_INLINE_PLUGIN_NAME,
|
||||||
|
configureVitest(context: {
|
||||||
|
experimental_defineCacheKeyGenerator(callback: typeof cacheKeyForSchema): void;
|
||||||
|
}) {
|
||||||
|
context.experimental_defineCacheKeyGenerator(cacheKeyForSchema);
|
||||||
|
},
|
||||||
load(this: { addWatchFile(id: string): void }, id: string) {
|
load(this: { addWatchFile(id: string): void }, id: string) {
|
||||||
const schema = schemasByModulePath.get(path.resolve(id));
|
const schema = schemasByModulePath.get(path.resolve(id));
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
|
|||||||
+25
@@ -250,6 +250,31 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
expect(out).toHaveLength(1);
|
expect(out).toHaveLength(1);
|
||||||
expect(out[0]?.role).toBe("user");
|
expect(out[0]?.role).toBe("user");
|
||||||
});
|
});
|
||||||
|
it.each([
|
||||||
|
["full", "length"],
|
||||||
|
["images-only", "length"],
|
||||||
|
["full", "error"],
|
||||||
|
["images-only", "error"],
|
||||||
|
] as const)(
|
||||||
|
"preserves an empty provider replay owner in %s mode after %s",
|
||||||
|
async (sanitizeMode, stopReason) => {
|
||||||
|
const checkpoint = {
|
||||||
|
...makeOpenAiResponsesAssistantMessage([{ type: "text", text: "" }], stopReason),
|
||||||
|
providerReplay: {
|
||||||
|
v: 1,
|
||||||
|
type: "opaque-checkpoint",
|
||||||
|
data: "opaque-state",
|
||||||
|
provider: "openai",
|
||||||
|
api: "openai-responses",
|
||||||
|
model: "gpt-5.4",
|
||||||
|
},
|
||||||
|
} satisfies AssistantMessage;
|
||||||
|
|
||||||
|
const out = await sanitizeSessionMessagesImages([checkpoint], "test", { sanitizeMode });
|
||||||
|
|
||||||
|
expect(out).toEqual([{ ...checkpoint, content: [] }]);
|
||||||
|
},
|
||||||
|
);
|
||||||
it("drops empty assistant error messages", async () => {
|
it("drops empty assistant error messages", async () => {
|
||||||
const input = castAgentMessages([
|
const input = castAgentMessages([
|
||||||
{ role: "user", content: "hello", timestamp: nextTimestamp() } satisfies UserMessage,
|
{ role: "user", content: "hello", timestamp: nextTimestamp() } satisfies UserMessage,
|
||||||
|
|||||||
@@ -53,8 +53,6 @@ export async function sanitizeSessionMessagesImages(
|
|||||||
};
|
};
|
||||||
} & ImageSanitizationLimits,
|
} & ImageSanitizationLimits,
|
||||||
): Promise<AgentMessage[]> {
|
): Promise<AgentMessage[]> {
|
||||||
const sanitizeMode = options?.sanitizeMode ?? "full";
|
|
||||||
const allowNonImageSanitization = sanitizeMode === "full";
|
|
||||||
const imageSanitization = {
|
const imageSanitization = {
|
||||||
maxDimensionPx: options?.maxDimensionPx,
|
maxDimensionPx: options?.maxDimensionPx,
|
||||||
maxBytes: options?.maxBytes,
|
maxBytes: options?.maxBytes,
|
||||||
@@ -113,7 +111,7 @@ export async function sanitizeSessionMessagesImages(
|
|||||||
imageSanitization,
|
imageSanitization,
|
||||||
)) as unknown as typeof assistantMsg.content;
|
)) as unknown as typeof assistantMsg.content;
|
||||||
const finalContent = dropEmptyTextBlocks(nextContent);
|
const finalContent = dropEmptyTextBlocks(nextContent);
|
||||||
if (finalContent.length > 0) {
|
if (finalContent.length > 0 || assistantMsg.providerReplay) {
|
||||||
out.push({ ...assistantMsg, content: finalContent });
|
out.push({ ...assistantMsg, content: finalContent });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -126,28 +124,14 @@ export async function sanitizeSessionMessagesImages(
|
|||||||
const strippedContent = options?.preserveSignatures
|
const strippedContent = options?.preserveSignatures
|
||||||
? content // Keep signatures for Antigravity Claude
|
? content // Keep signatures for Antigravity Claude
|
||||||
: stripThoughtSignatures(content, options?.sanitizeThoughtSignatures); // Strip for Gemini
|
: stripThoughtSignatures(content, options?.sanitizeThoughtSignatures); // Strip for Gemini
|
||||||
if (!allowNonImageSanitization) {
|
|
||||||
const nextContent = (await sanitizeContentBlocksImages(
|
|
||||||
dropEmptyTextBlocks(strippedContent) as unknown as ContentBlock[],
|
|
||||||
label,
|
|
||||||
imageSanitization,
|
|
||||||
)) as unknown as typeof assistantMsg.content;
|
|
||||||
if (nextContent.length > 0) {
|
|
||||||
out.push({ ...assistantMsg, content: nextContent });
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredContent = dropEmptyTextBlocks(strippedContent);
|
|
||||||
const finalContent = (await sanitizeContentBlocksImages(
|
const finalContent = (await sanitizeContentBlocksImages(
|
||||||
filteredContent as unknown as ContentBlock[],
|
dropEmptyTextBlocks(strippedContent) as unknown as ContentBlock[],
|
||||||
label,
|
label,
|
||||||
imageSanitization,
|
imageSanitization,
|
||||||
)) as unknown as typeof assistantMsg.content;
|
)) as unknown as typeof assistantMsg.content;
|
||||||
if (finalContent.length === 0) {
|
if (finalContent.length > 0 || assistantMsg.providerReplay) {
|
||||||
continue;
|
out.push({ ...assistantMsg, content: finalContent });
|
||||||
}
|
}
|
||||||
out.push({ ...assistantMsg, content: finalContent });
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,6 +134,48 @@ describe("runEmbeddedAgent incomplete-turn safety", () => {
|
|||||||
expectWarnMessageWith("empty response detected");
|
expectWarnMessageWith("empty response detected");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("continues after an OpenAI Responses compaction-only incomplete turn", async () => {
|
||||||
|
const checkpoint = makeLastAssistant({
|
||||||
|
api: "openai-responses",
|
||||||
|
provider: "openai",
|
||||||
|
model: "gpt-5.6-luna",
|
||||||
|
stopReason: "length",
|
||||||
|
providerReplay: {
|
||||||
|
v: 1,
|
||||||
|
type: "openai-responses-compaction",
|
||||||
|
data: "opaque-checkpoint",
|
||||||
|
provider: "openai",
|
||||||
|
api: "openai-responses",
|
||||||
|
model: "gpt-5.6-luna",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mockedRunEmbeddedAttempt.mockResolvedValueOnce(
|
||||||
|
makeAttemptResult({
|
||||||
|
assistantTexts: [],
|
||||||
|
currentAttemptAssistant: checkpoint,
|
||||||
|
lastAssistant: checkpoint,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
mockedRunEmbeddedAttempt.mockResolvedValueOnce(
|
||||||
|
makeAttemptResult({
|
||||||
|
assistantTexts: ["Visible answer after compaction."],
|
||||||
|
lastAssistant: makeLastAssistant({
|
||||||
|
content: [{ type: "text", text: "Visible answer after compaction." }],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await runEmbeddedAgent(
|
||||||
|
makeRunParams("run-provider-compaction-continuation", {
|
||||||
|
provider: "openai",
|
||||||
|
model: "gpt-5.6-luna",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
|
||||||
|
expectWarnMessageWith("compaction interrupted visible final answer");
|
||||||
|
});
|
||||||
|
|
||||||
it("retries empty Anthropic-compatible stop turns even when the provider is not Kimi", async () => {
|
it("retries empty Anthropic-compatible stop turns even when the provider is not Kimi", async () => {
|
||||||
mockedClassifyFailoverReason.mockReturnValue(null);
|
mockedClassifyFailoverReason.mockReturnValue(null);
|
||||||
mockedResolveModelAsync.mockResolvedValue({
|
mockedResolveModelAsync.mockResolvedValue({
|
||||||
|
|||||||
@@ -341,7 +341,8 @@ export async function resolveEmbeddedRunTerminal(input: {
|
|||||||
if (
|
if (
|
||||||
!emptyAssistantReplyIsSilent &&
|
!emptyAssistantReplyIsSilent &&
|
||||||
!settledTurnFinalizationAttempted &&
|
!settledTurnFinalizationAttempted &&
|
||||||
input.attemptCompactionCount > 0 &&
|
(input.attemptCompactionCount > 0 ||
|
||||||
|
attempt.currentAttemptAssistant?.providerReplay?.type === "openai-responses-compaction") &&
|
||||||
payloadCount === 0 &&
|
payloadCount === 0 &&
|
||||||
!terminalInterrupted &&
|
!terminalInterrupted &&
|
||||||
!promptError &&
|
!promptError &&
|
||||||
|
|||||||
@@ -62,19 +62,21 @@ describe("gateway codex harness live helpers", () => {
|
|||||||
guardianProbe: false,
|
guardianProbe: false,
|
||||||
imageProbe: false,
|
imageProbe: false,
|
||||||
mcpProbe: false,
|
mcpProbe: false,
|
||||||
|
multiSessionProbe: false,
|
||||||
resumeStress: false,
|
resumeStress: false,
|
||||||
subagentProbe: true,
|
subagentProbe: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(shouldUseCodexHarnessSubagentOnlyFastPath(base)).toBe(true);
|
expect(shouldUseCodexHarnessSubagentOnlyFastPath(base)).toBe(true);
|
||||||
expect(shouldUseCodexHarnessSubagentOnlyFastPath({ ...base, resumeStress: true })).toBe(false);
|
for (const flag of [
|
||||||
expect(shouldUseCodexHarnessSubagentOnlyFastPath({ ...base, compactionStress: true })).toBe(
|
"codeModeOnly",
|
||||||
false,
|
"compactionStress",
|
||||||
);
|
"explicitOptOut",
|
||||||
expect(shouldUseCodexHarnessSubagentOnlyFastPath({ ...base, codeModeOnly: true })).toBe(false);
|
"multiSessionProbe",
|
||||||
expect(shouldUseCodexHarnessSubagentOnlyFastPath({ ...base, explicitOptOut: true })).toBe(
|
"resumeStress",
|
||||||
false,
|
] as const) {
|
||||||
);
|
expect(shouldUseCodexHarnessSubagentOnlyFastPath({ ...base, [flag]: true })).toBe(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("classifies sessions.list timeouts as retryable live Codex errors", () => {
|
it("classifies sessions.list timeouts as retryable live Codex errors", () => {
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export function shouldUseCodexHarnessSubagentOnlyFastPath(params: {
|
|||||||
guardianProbe: boolean;
|
guardianProbe: boolean;
|
||||||
imageProbe: boolean;
|
imageProbe: boolean;
|
||||||
mcpProbe: boolean;
|
mcpProbe: boolean;
|
||||||
|
multiSessionProbe: boolean;
|
||||||
resumeStress: boolean;
|
resumeStress: boolean;
|
||||||
subagentProbe: boolean;
|
subagentProbe: boolean;
|
||||||
}): boolean {
|
}): boolean {
|
||||||
@@ -107,6 +108,7 @@ export function shouldUseCodexHarnessSubagentOnlyFastPath(params: {
|
|||||||
!params.guardianProbe &&
|
!params.guardianProbe &&
|
||||||
!params.imageProbe &&
|
!params.imageProbe &&
|
||||||
!params.mcpProbe &&
|
!params.mcpProbe &&
|
||||||
|
!params.multiSessionProbe &&
|
||||||
!params.resumeStress &&
|
!params.resumeStress &&
|
||||||
!params.explicitOptOut
|
!params.explicitOptOut
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ const CODEX_HARNESS_SUBAGENT_ONLY = shouldUseCodexHarnessSubagentOnlyFastPath({
|
|||||||
guardianProbe: CODEX_HARNESS_GUARDIAN_PROBE,
|
guardianProbe: CODEX_HARNESS_GUARDIAN_PROBE,
|
||||||
imageProbe: CODEX_HARNESS_IMAGE_PROBE,
|
imageProbe: CODEX_HARNESS_IMAGE_PROBE,
|
||||||
mcpProbe: CODEX_HARNESS_MCP_PROBE,
|
mcpProbe: CODEX_HARNESS_MCP_PROBE,
|
||||||
|
multiSessionProbe: CODEX_HARNESS_MULTI_SESSION_PROBE,
|
||||||
resumeStress: CODEX_HARNESS_RESUME_STRESS,
|
resumeStress: CODEX_HARNESS_RESUME_STRESS,
|
||||||
subagentProbe: CODEX_HARNESS_SUBAGENT_PROBE,
|
subagentProbe: CODEX_HARNESS_SUBAGENT_PROBE,
|
||||||
});
|
});
|
||||||
@@ -2209,7 +2210,6 @@ describeLive("gateway live (Codex harness)", () => {
|
|||||||
},
|
},
|
||||||
workspace,
|
workspace,
|
||||||
});
|
});
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CODEX_HARNESS_SUBAGENT_PROBE) {
|
if (CODEX_HARNESS_SUBAGENT_PROBE) {
|
||||||
|
|||||||
@@ -503,8 +503,12 @@ describeLive("Gateway OpenAI long-context compaction (live)", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!compactionState?.latest) {
|
if (!compactionState?.latest) {
|
||||||
|
const thresholdEvidence =
|
||||||
|
peakPromptTokens > 0
|
||||||
|
? `peak provider prompt tokens=${peakPromptTokens}, compact threshold=${profile.compactThreshold}`
|
||||||
|
: "provider prompt-token usage unavailable";
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`OpenAI emitted no first-class compaction item after ${profile.maxDenseTurns} dense turns`,
|
`OpenAI emitted no first-class compaction item after ${profile.maxDenseTurns} dense turns; ${thresholdEvidence}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
expect(compactionState.latest).toMatchObject({
|
expect(compactionState.latest).toMatchObject({
|
||||||
|
|||||||
@@ -111,6 +111,12 @@ describe("tsdown config", () => {
|
|||||||
const rootDir = process.cwd();
|
const rootDir = process.cwd();
|
||||||
const watchedPaths: string[] = [];
|
const watchedPaths: string[] = [];
|
||||||
const plugin = createStateSchemaInlinePlugin(rootDir);
|
const plugin = createStateSchemaInlinePlugin(rootDir);
|
||||||
|
let cacheKeyGenerator: ((context: { id: string }) => string | undefined) | undefined;
|
||||||
|
plugin.configureVitest({
|
||||||
|
experimental_defineCacheKeyGenerator: (generator) => {
|
||||||
|
cacheKeyGenerator = generator;
|
||||||
|
},
|
||||||
|
});
|
||||||
const result = plugin.load.call(
|
const result = plugin.load.call(
|
||||||
{ addWatchFile: (filePath: string) => watchedPaths.push(filePath) },
|
{ addWatchFile: (filePath: string) => watchedPaths.push(filePath) },
|
||||||
path.resolve(rootDir, schema.modulePath),
|
path.resolve(rootDir, schema.modulePath),
|
||||||
@@ -126,6 +132,10 @@ describe("tsdown config", () => {
|
|||||||
expect(JSON.parse(match?.[1] ?? "null")).toBe(canonicalSql);
|
expect(JSON.parse(match?.[1] ?? "null")).toBe(canonicalSql);
|
||||||
expect(schema.sourceValue).toBe(canonicalSql);
|
expect(schema.sourceValue).toBe(canonicalSql);
|
||||||
expect(watchedPaths).toEqual([schemaPath]);
|
expect(watchedPaths).toEqual([schemaPath]);
|
||||||
|
expect(cacheKeyGenerator?.({ id: path.resolve(rootDir, schema.modulePath) })).toBe(
|
||||||
|
canonicalSql,
|
||||||
|
);
|
||||||
|
expect(cacheKeyGenerator?.({ id: path.resolve(rootDir, "src/index.ts") })).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("installs schema inlining only on the unified runtime graph", () => {
|
it("installs schema inlining only on the unified runtime graph", () => {
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ describe("Gateway OpenAI Responses compaction replay", () => {
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await runAgentTurn(client, "capture compaction state");
|
await runAgentTurn(client, "capture compaction state");
|
||||||
expect(modelServer.requests).toHaveLength(1);
|
// The provider can terminate after emitting only a compaction item. The
|
||||||
|
// runner must continue from that checkpoint before completing the turn.
|
||||||
|
expect(modelServer.requests).toHaveLength(2);
|
||||||
|
|
||||||
const session = await client.request<{
|
const session = await client.request<{
|
||||||
sessions?: Array<{ key?: string; sessionId?: string }>;
|
sessions?: Array<{ key?: string; sessionId?: string }>;
|
||||||
@@ -78,28 +80,31 @@ describe("Gateway OpenAI Responses compaction replay", () => {
|
|||||||
sessionKey: SESSION_KEY,
|
sessionKey: SESSION_KEY,
|
||||||
storePath: path.join(instance.state.agentDir("main"), "openclaw-agent.sqlite"),
|
storePath: path.join(instance.state.agentDir("main"), "openclaw-agent.sqlite"),
|
||||||
});
|
});
|
||||||
const persistedReplay = manager
|
const contextMessages = manager.buildSessionContext().messages;
|
||||||
.buildSessionContext()
|
const persistedReplay = contextMessages.find(
|
||||||
.messages.find((message) => message.role === "assistant")?.providerReplay;
|
(message) => message.role === "assistant",
|
||||||
|
)?.providerReplay;
|
||||||
expect(persistedReplay).toMatchObject({
|
expect(persistedReplay).toMatchObject({
|
||||||
|
v: 1,
|
||||||
type: "openai-responses-compaction",
|
type: "openai-responses-compaction",
|
||||||
id: COMPACTION_ID,
|
id: COMPACTION_ID,
|
||||||
data: COMPACTION_DATA,
|
data: COMPACTION_DATA,
|
||||||
provider: "replay-proof",
|
provider: "replay-proof",
|
||||||
api: "openai-responses",
|
api: "openai-responses",
|
||||||
model: "replay-proof",
|
model: "replay-proof",
|
||||||
|
baseUrlHash: expect.any(String),
|
||||||
sessionHash: expect.any(String),
|
sessionHash: expect.any(String),
|
||||||
});
|
});
|
||||||
expect(persistedReplay).not.toHaveProperty("authProfileHash");
|
expect(persistedReplay).not.toHaveProperty("authProfileHash");
|
||||||
|
expectCompactionReplay(modelServer.requests[1]?.body.input ?? []);
|
||||||
|
expect(JSON.stringify(modelServer.requests[1]?.body.input)).toContain(
|
||||||
|
"Continue from the compacted transcript",
|
||||||
|
);
|
||||||
|
|
||||||
await runAgentTurn(client, "replay compaction state");
|
await runAgentTurn(client, "replay compaction state");
|
||||||
expect(modelServer.requests).toHaveLength(2);
|
expect(modelServer.requests).toHaveLength(3);
|
||||||
const replayInput = modelServer.requests[1]?.body.input ?? [];
|
const replayInput = modelServer.requests[2]?.body.input ?? [];
|
||||||
expect(replayInput).toContainEqual({
|
expectCompactionReplay(replayInput);
|
||||||
type: "compaction",
|
|
||||||
id: COMPACTION_ID,
|
|
||||||
encrypted_content: COMPACTION_DATA,
|
|
||||||
});
|
|
||||||
const compactionIndex = replayInput.findIndex(
|
const compactionIndex = replayInput.findIndex(
|
||||||
(item) =>
|
(item) =>
|
||||||
typeof item === "object" &&
|
typeof item === "object" &&
|
||||||
@@ -120,7 +125,7 @@ describe("Gateway OpenAI Responses compaction replay", () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
const encodedReplayInput = JSON.stringify(replayInput);
|
const encodedReplayInput = JSON.stringify(replayInput);
|
||||||
expect(encodedReplayInput).not.toContain("capture compaction state");
|
expect(encodedReplayInput).not.toContain("capture compaction state");
|
||||||
expect(encodedReplayInput).toContain("gateway replay response 1");
|
expect(encodedReplayInput).toContain("gateway replay response 2");
|
||||||
expect(encodedReplayInput).toContain("replay compaction state");
|
expect(encodedReplayInput).toContain("replay compaction state");
|
||||||
} finally {
|
} finally {
|
||||||
await disconnectGatewayClient(client);
|
await disconnectGatewayClient(client);
|
||||||
@@ -188,6 +193,14 @@ async function runAgentTurn(
|
|||||||
return runId;
|
return runId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function expectCompactionReplay(input: unknown[]): void {
|
||||||
|
expect(input).toContainEqual({
|
||||||
|
type: "compaction",
|
||||||
|
id: COMPACTION_ID,
|
||||||
|
encrypted_content: COMPACTION_DATA,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function startMockModelServer(): Promise<MockModelServer> {
|
async function startMockModelServer(): Promise<MockModelServer> {
|
||||||
const requests: CapturedRequest[] = [];
|
const requests: CapturedRequest[] = [];
|
||||||
const server = createServer((request, response) => {
|
const server = createServer((request, response) => {
|
||||||
@@ -241,6 +254,28 @@ async function handleRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function writeModelResponse(response: ServerResponse, sequence: number): void {
|
function writeModelResponse(response: ServerResponse, sequence: number): void {
|
||||||
|
if (sequence === 1) {
|
||||||
|
const compaction = {
|
||||||
|
type: "compaction",
|
||||||
|
id: COMPACTION_ID,
|
||||||
|
encrypted_content: COMPACTION_DATA,
|
||||||
|
};
|
||||||
|
writeSseEvents(response, [
|
||||||
|
{ type: "response.output_item.added", output_index: 0, item: compaction },
|
||||||
|
{ type: "response.output_item.done", output_index: 0, item: compaction },
|
||||||
|
{
|
||||||
|
type: "response.incomplete",
|
||||||
|
response: {
|
||||||
|
id: "resp_gateway_replay_1",
|
||||||
|
status: "incomplete",
|
||||||
|
incomplete_details: { reason: "max_output_tokens" },
|
||||||
|
output: [compaction],
|
||||||
|
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const text = `gateway replay response ${sequence}`;
|
const text = `gateway replay response ${sequence}`;
|
||||||
const message = {
|
const message = {
|
||||||
type: "message",
|
type: "message",
|
||||||
@@ -249,10 +284,7 @@ function writeModelResponse(response: ServerResponse, sequence: number): void {
|
|||||||
status: "completed",
|
status: "completed",
|
||||||
content: [{ type: "output_text", text, annotations: [] }],
|
content: [{ type: "output_text", text, annotations: [] }],
|
||||||
};
|
};
|
||||||
const output =
|
const output = [message];
|
||||||
sequence === 1
|
|
||||||
? [{ type: "compaction", id: COMPACTION_ID, encrypted_content: COMPACTION_DATA }, message]
|
|
||||||
: [message];
|
|
||||||
const events: MockSseEvent[] = output.flatMap((item, outputIndex) => [
|
const events: MockSseEvent[] = output.flatMap((item, outputIndex) => [
|
||||||
{
|
{
|
||||||
type: "response.output_item.added",
|
type: "response.output_item.added",
|
||||||
@@ -270,6 +302,10 @@ function writeModelResponse(response: ServerResponse, sequence: number): void {
|
|||||||
usage: { input_tokens: 10, output_tokens: 2, total_tokens: 12 },
|
usage: { input_tokens: 10, output_tokens: 2, total_tokens: 12 },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
writeSseEvents(response, events);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeSseEvents(response: ServerResponse, events: MockSseEvent[]): void {
|
||||||
response.writeHead(200, {
|
response.writeHead(200, {
|
||||||
"content-type": "text/event-stream",
|
"content-type": "text/event-stream",
|
||||||
"cache-control": "no-store",
|
"cache-control": "no-store",
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ describe("OpenAI long-context live settings", () => {
|
|||||||
contextWindow: 48_000,
|
contextWindow: 48_000,
|
||||||
contextTokens: 48_000,
|
contextTokens: 48_000,
|
||||||
maxTokens: 8_192,
|
maxTokens: 8_192,
|
||||||
compactThreshold: 32_000,
|
compactThreshold: 1_000,
|
||||||
});
|
});
|
||||||
const full = resolveOpenAILongContextLiveSettings(
|
const full = resolveOpenAILongContextLiveSettings(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -48,9 +48,12 @@ const PROFILES = {
|
|||||||
contextWindow: 48_000,
|
contextWindow: 48_000,
|
||||||
contextTokens: 48_000,
|
contextTokens: 48_000,
|
||||||
maxTokens: 8_192,
|
maxTokens: 8_192,
|
||||||
compactThreshold: 32_000,
|
// Keep the reduced live probe on OpenAI's demonstrated compaction path.
|
||||||
|
// High-threshold Luna probes can cross the configured threshold without
|
||||||
|
// emitting a checkpoint, while the 1k boundary is deterministic.
|
||||||
|
compactThreshold: 1_000,
|
||||||
denseTurnChars: 120_000,
|
denseTurnChars: 120_000,
|
||||||
maxDenseTurns: 8,
|
maxDenseTurns: 3,
|
||||||
defaultToolBytes: 300_000,
|
defaultToolBytes: 300_000,
|
||||||
requestTimeoutMs: 2 * 60_000,
|
requestTimeoutMs: 2 * 60_000,
|
||||||
suiteTimeoutMs: 10 * 60_000,
|
suiteTimeoutMs: 10 * 60_000,
|
||||||
@@ -199,6 +202,9 @@ export function buildOpenAILongContextConfig(params: {
|
|||||||
workspace: params.workspace,
|
workspace: params.workspace,
|
||||||
skipBootstrap: true,
|
skipBootstrap: true,
|
||||||
thinkingDefault: "low",
|
thinkingDefault: "low",
|
||||||
|
// This suite owns the server-compaction threshold. Embedded proactive
|
||||||
|
// compaction would consume the same history before replay can be proved.
|
||||||
|
compaction: { enabled: false },
|
||||||
model: { primary: profile.modelRef },
|
model: { primary: profile.modelRef },
|
||||||
models: {
|
models: {
|
||||||
[profile.modelRef]: {
|
[profile.modelRef]: {
|
||||||
@@ -256,6 +262,11 @@ export function assertOpenAILongContextConfig(
|
|||||||
cfg.secrets?.providers?.default?.source,
|
cfg.secrets?.providers?.default?.source,
|
||||||
"env",
|
"env",
|
||||||
);
|
);
|
||||||
|
expectConfigValue(
|
||||||
|
"agents.defaults.compaction.enabled",
|
||||||
|
cfg.agents?.defaults?.compaction?.enabled,
|
||||||
|
false,
|
||||||
|
);
|
||||||
expectConfigValue("models.providers.openai.models.length", provider?.models.length, 1);
|
expectConfigValue("models.providers.openai.models.length", provider?.models.length, 1);
|
||||||
const model = provider?.models[0];
|
const model = provider?.models[0];
|
||||||
expectConfigValue("model.id", model?.id, profile.modelId);
|
expectConfigValue("model.id", model?.id, profile.modelId);
|
||||||
|
|||||||
Reference in New Issue
Block a user