test: trim provider runtime from agents hotspots

This commit is contained in:
Peter Steinberger
2026-04-10 15:56:03 +01:00
parent e9fb4c7f93
commit 3522224b25
14 changed files with 229 additions and 151 deletions

View File

@@ -71,7 +71,7 @@ function buildDiscordCleanupHooks(onDelete: (key: string | undefined) => void) {
};
}
const waitFor = async (predicate: () => boolean, timeoutMs = 1_500) => {
const waitFor = async (predicate: () => boolean, timeoutMs = 3_000) => {
await vi.waitFor(
() => {
expect(predicate()).toBe(true);
@@ -103,7 +103,7 @@ async function executeSpawnAndExpectAccepted(params: {
});
expect(result.details).toMatchObject({
status: "accepted",
runId: "run-1",
runId: expect.any(String),
});
return result;
}
@@ -131,6 +131,13 @@ async function emitLifecycleEndAndFlush(params: {
}
}
async function waitForRunCleanup(childSessionKey: string) {
await waitFor(() => {
const run = getLatestSubagentRunByChildSessionKey(childSessionKey);
return run?.cleanupCompletedAt != null;
});
}
describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
beforeEach(() => {
resetSessionsSpawnAnnounceFlowOverride();
@@ -147,7 +154,7 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
},
},
});
resetSubagentRegistryForTests();
resetSubagentRegistryForTests({ persist: false });
hookRunnerMocks.runSubagentSpawning.mockClear();
hookRunnerMocks.runSubagentSpawned.mockClear();
hookRunnerMocks.runSubagentEnded.mockClear();
@@ -167,7 +174,7 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
resetSessionsSpawnAnnounceFlowOverride();
resetSessionsSpawnHookRunnerOverride();
resetSessionsSpawnConfigOverride();
resetSubagentRegistryForTests();
resetSubagentRegistryForTests({ persist: false });
});
afterAll(() => {
@@ -211,6 +218,10 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
patchCalls.some((call) => call.label === "my-task") &&
ctx.calls.filter((call) => call.method === "agent").length >= 2,
);
if (!child.sessionKey) {
throw new Error("missing child sessionKey");
}
await waitForRunCleanup(child.sessionKey);
const childWait = ctx.waitCalls.find((call) => call.runId === child.runId);
expect(childWait?.timeoutMs).toBe(1000);
@@ -387,6 +398,7 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
getLatestSubagentRunByChildSessionKey(childSessionKey)?.outcome?.status === "timeout"
);
}, 20_000);
await waitForRunCleanup(childSessionKey);
const childWait = ctx.waitCalls.find((call) => call.runId === child.runId);
expect(childWait?.timeoutMs).toBe(1000);
@@ -412,13 +424,11 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
if (!child.runId) {
throw new Error("missing child runId");
}
await emitLifecycleEndAndFlush({
runId: child.runId,
startedAt: 1000,
endedAt: 2000,
});
if (!child.sessionKey) {
throw new Error("missing child sessionKey");
}
await waitFor(() => ctx.calls.filter((call) => call.method === "agent").length >= 2);
await waitForRunCleanup(child.sessionKey);
const agentCalls = ctx.calls.filter((call) => call.method === "agent");
expect(agentCalls).toHaveLength(2);

View File

@@ -26,6 +26,7 @@ type SessionsSpawnGatewayMockOptions = {
const hoisted = vi.hoisted(() => {
const callGatewayMock = vi.fn();
let nextRunId = 0;
const defaultConfigOverride = {
session: {
mainKey: "main",
@@ -88,7 +89,15 @@ const hoisted = vi.hoisted(() => {
defaultRunSubagentAnnounceFlow,
runSubagentAnnounceFlowOverride: defaultRunSubagentAnnounceFlow,
};
return { callGatewayMock, defaultConfigOverride, state };
return {
callGatewayMock,
defaultConfigOverride,
nextRunId: () => {
nextRunId += 1;
return `run-${nextRunId}`;
},
state,
};
});
let cachedCreateSessionsSpawnTool: CreateSessionsSpawnTool | null = null;
@@ -143,6 +152,7 @@ export async function getSessionsSpawnTool(opts: CreateOpenClawToolsOpts) {
subagentRegistryTesting.setDepsForTest({
callGateway: (optsUnknown) => hoisted.callGatewayMock(optsUnknown),
loadConfig: () => hoisted.state.configOverride,
cleanupBrowserSessionsForLifecycleEnd: async () => {},
captureSubagentCompletionReply: (sessionKey) =>
hoisted.state.captureSubagentCompletionReplyOverride(sessionKey),
runSubagentAnnounceFlow: (params) => hoisted.state.runSubagentAnnounceFlowOverride(params),
@@ -161,7 +171,6 @@ export function setupSessionsSpawnGatewayMock(setupOpts: SessionsSpawnGatewayMoc
} {
const calls: Array<GatewayRequest> = [];
const waitCalls: Array<AgentWaitCall> = [];
let agentCallCount = 0;
let childRunId: string | undefined;
let childSessionKey: string | undefined;
@@ -182,8 +191,7 @@ export function setupSessionsSpawnGatewayMock(setupOpts: SessionsSpawnGatewayMoc
}
if (request.method === "agent") {
agentCallCount += 1;
const runId = `run-${agentCallCount}`;
const runId = hoisted.nextRunId();
const params = request.params as { lane?: string; sessionKey?: string } | undefined;
// Capture only the subagent run metadata.
if (params?.lane === "subagent") {
@@ -194,7 +202,7 @@ export function setupSessionsSpawnGatewayMock(setupOpts: SessionsSpawnGatewayMoc
return {
runId,
status: "accepted",
acceptedAt: 1000 + agentCallCount,
acceptedAt: Date.now(),
};
}

View File

@@ -1,7 +1,7 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import { describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { isOpenRouterAnthropicModelRef } from "./anthropic-family-cache-semantics.js";
import { applyExtraParamsToAgent } from "./extra-params.js";
import { __testing as extraParamsTesting, applyExtraParamsToAgent } from "./extra-params.js";
import { resolveCacheRetention } from "./prompt-cache-retention.js";
function applyAndExpectWrapped(params: {
@@ -36,6 +36,17 @@ vi.mock("./logger.js", () => ({
},
}));
beforeEach(() => {
extraParamsTesting.setProviderRuntimeDepsForTest({
prepareProviderExtraParams: () => undefined,
wrapProviderStreamFn: () => undefined,
});
});
afterEach(() => {
extraParamsTesting.resetProviderRuntimeDepsForTest();
});
describe("cacheRetention default behavior", () => {
it("returns 'short' for Anthropic when not configured", () => {
applyAndExpectWrapped({

View File

@@ -25,6 +25,7 @@ function runOpenRouterPayload(payload: StreamPayload, modelId: string) {
provider: "openrouter",
id: modelId,
} as Model<"openai-completions">,
mockProviderRuntime: true,
payload,
});
}

View File

@@ -2,7 +2,7 @@ import type { StreamFn } from "@mariozechner/pi-agent-core";
import type { Context, Model, SimpleStreamOptions } from "@mariozechner/pi-ai";
import type { ThinkLevel } from "../../auto-reply/thinking.shared.js";
import type { OpenClawConfig } from "../../config/config.js";
import { applyExtraParamsToAgent } from "./extra-params.js";
import { __testing as extraParamsTesting, applyExtraParamsToAgent } from "./extra-params.js";
export type ExtraParamsCapture<TPayload extends Record<string, unknown>> = {
headers?: Record<string, string>;
@@ -31,6 +31,7 @@ type RunExtraParamsCaseParams<
callerHeaders?: Record<string, string>;
cfg?: OpenClawConfig;
model: Model<TApi>;
mockProviderRuntime?: boolean;
options?: SimpleStreamOptions;
payload: TPayload;
thinkingLevel?: ThinkLevel;
@@ -52,14 +53,26 @@ export function runExtraParamsCase<
};
const agent = { streamFn: baseStreamFn };
applyExtraParamsToAgent(
agent,
params.cfg,
params.applyProvider ?? params.model.provider,
params.applyModelId ?? params.model.id,
undefined,
params.thinkingLevel,
);
if (params.mockProviderRuntime === true) {
extraParamsTesting.setProviderRuntimeDepsForTest({
prepareProviderExtraParams: () => undefined,
wrapProviderStreamFn: () => undefined,
});
}
try {
applyExtraParamsToAgent(
agent,
params.cfg,
params.applyProvider ?? params.model.provider,
params.applyModelId ?? params.model.id,
undefined,
params.thinkingLevel,
);
} finally {
if (params.mockProviderRuntime === true) {
extraParamsTesting.resetProviderRuntimeDepsForTest();
}
}
const context: Context = { messages: [] };
void agent.streamFn?.(params.model, context, {

View File

@@ -34,6 +34,19 @@ vi.mock("../pi-model-discovery.js", () => ({
discoverModels: discoverModelsMock,
}));
vi.mock("../../plugins/provider-runtime.js", () => ({
applyProviderResolvedModelCompatWithPlugins: () => undefined,
applyProviderResolvedTransportWithPlugin: () => undefined,
buildProviderUnknownModelHintWithPlugin: () => undefined,
clearProviderRuntimeHookCache: () => {},
normalizeProviderResolvedModelWithPlugin: () => undefined,
normalizeProviderTransportWithPlugin: () => undefined,
prepareProviderDynamicModel: async () => {},
resolveProviderBuiltInModelSuppression: () => undefined,
runProviderDynamicModel: () => undefined,
shouldPreferProviderRuntimeResolvedModel: () => false,
}));
describe("resolveModelAsync startup retry", () => {
const runtimeHooks = {
applyProviderResolvedModelCompatWithPlugins: () => undefined,

View File

@@ -1,5 +1,4 @@
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import { DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH } from "../config/agent-limits.js";
import { defaultRuntime } from "../runtime.js";
import { isCronSessionKey } from "../sessions/session-key-utils.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
@@ -61,116 +60,7 @@ function loadSubagentRegistryRuntime() {
return subagentRegistryRuntimePromise;
}
export function buildSubagentSystemPrompt(params: {
requesterSessionKey?: string;
requesterOrigin?: DeliveryContext;
childSessionKey: string;
label?: string;
task?: string;
/** Whether ACP-specific routing guidance should be included. Defaults to true. */
acpEnabled?: boolean;
/** Depth of the child being spawned (1 = sub-agent, 2 = sub-sub-agent). */
childDepth?: number;
/** Config value: max allowed spawn depth. */
maxSpawnDepth?: number;
}) {
const taskText =
typeof params.task === "string" && params.task.trim()
? params.task.replace(/\s+/g, " ").trim()
: "{{TASK_DESCRIPTION}}";
const childDepth = typeof params.childDepth === "number" ? params.childDepth : 1;
const maxSpawnDepth =
typeof params.maxSpawnDepth === "number"
? params.maxSpawnDepth
: DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH;
const acpEnabled = params.acpEnabled !== false;
const canSpawn = childDepth < maxSpawnDepth;
const parentLabel = childDepth >= 2 ? "parent orchestrator" : "main agent";
const lines = [
"# Subagent Context",
"",
`You are a **subagent** spawned by the ${parentLabel} for a specific task.`,
"",
"## Your Role",
`- You were created to handle: ${taskText}`,
"- Complete this task. That's your entire purpose.",
`- You are NOT the ${parentLabel}. Don't try to be.`,
"",
"## Rules",
"1. **Stay focused** - Do your assigned task, nothing else",
`2. **Complete the task** - Your final message will be automatically reported to the ${parentLabel}`,
"3. **Don't initiate** - No heartbeats, no proactive actions, no side quests",
"4. **Be ephemeral** - You may be terminated after task completion. That's fine.",
"5. **Trust push-based completion** - Descendant results are auto-announced back to you; do not busy-poll for status.",
"6. **Recover from truncated tool output** - If you see a notice like `[... N more characters truncated]`, assume prior output was reduced. Re-read only what you need using smaller chunks (`read` with offset/limit, or targeted `rg`/`head`/`tail`) instead of full-file `cat`.",
"",
"## Output Format",
"When complete, your final response should include:",
`- What you accomplished or found`,
`- Any relevant details the ${parentLabel} should know`,
"- Keep it concise but informative",
"",
"## What You DON'T Do",
`- NO user conversations (that's ${parentLabel}'s job)`,
"- NO external messages (email, tweets, etc.) unless explicitly tasked with a specific recipient/channel",
"- NO cron jobs or persistent state",
`- NO pretending to be the ${parentLabel}`,
`- Only use the \`message\` tool when explicitly instructed to contact a specific external recipient; otherwise return plain text and let the ${parentLabel} deliver it`,
"",
];
if (canSpawn) {
lines.push(
"## Sub-Agent Spawning",
"You CAN spawn your own sub-agents for parallel or complex work using `sessions_spawn`.",
"Use the `subagents` tool to steer, kill, or do an on-demand status check for your spawned sub-agents.",
"Your sub-agents will announce their results back to you automatically (not to the main agent).",
"Default workflow: spawn work, continue orchestrating, and wait for auto-announced completions.",
"Auto-announce is push-based. After spawning children, do NOT call sessions_list, sessions_history, exec sleep, or any polling tool.",
"Wait for completion events to arrive as user messages.",
"Track expected child session keys and only send your final answer after completion events for ALL expected children arrive.",
"If a child completion event arrives AFTER you already sent your final answer, reply ONLY with NO_REPLY.",
"Do NOT repeatedly poll `subagents list` in a loop unless you are actively debugging or intervening.",
"Coordinate their work and synthesize results before reporting back.",
...(acpEnabled
? [
'For ACP harness sessions (codex/claudecode/gemini), use `sessions_spawn` with `runtime: "acp"` (set `agentId` unless `acp.defaultAgent` is configured).',
'`agents_list` and `subagents` apply to OpenClaw sub-agents (`runtime: "subagent"`); ACP harness ids are controlled by `acp.allowedAgents`.',
"Do not ask users to run slash commands or CLI when `sessions_spawn` can do it directly.",
"Do not use `exec` (`openclaw ...`, `acpx ...`) to spawn ACP sessions.",
'Use `subagents` only for OpenClaw subagents (`runtime: "subagent"`).',
"Subagent results auto-announce back to you; ACP sessions continue in their bound thread.",
"Avoid polling loops; spawn, orchestrate, and synthesize results.",
]
: []),
"",
);
} else if (childDepth >= 2) {
lines.push(
"## Sub-Agent Spawning",
"You are a leaf worker and CANNOT spawn further sub-agents. Focus on your assigned task.",
"",
);
}
lines.push(
"## Session Context",
...[
params.label ? `- Label: ${params.label}` : undefined,
params.requesterSessionKey
? `- Requester session: ${params.requesterSessionKey}.`
: undefined,
params.requesterOrigin?.channel
? `- Requester channel: ${params.requesterOrigin.channel}.`
: undefined,
`- Your session: ${params.childSessionKey}.`,
].filter((line): line is string => line !== undefined),
"",
);
return lines.join("\n");
}
export { buildSubagentSystemPrompt } from "./subagent-system-prompt.js";
export { captureSubagentCompletionReply } from "./subagent-announce-output.js";
export type { SubagentRunOutcome } from "./subagent-announce-output.js";

View File

@@ -61,6 +61,7 @@ export function createSubagentRegistryLifecycleController(params: {
}): Promise<void>;
resumeSubagentRun(runId: string): void;
captureSubagentCompletionReply: typeof captureSubagentCompletionReply;
cleanupBrowserSessionsForLifecycleEnd?: typeof cleanupBrowserSessionsForLifecycleEnd;
runSubagentAnnounceFlow: typeof runSubagentAnnounceFlow;
warn(message: string, meta?: Record<string, unknown>): void;
}) {
@@ -608,7 +609,7 @@ export function createSubagentRegistryLifecycleController(params: {
return;
}
await cleanupBrowserSessionsForLifecycleEnd({
await (params.cleanupBrowserSessionsForLifecycleEnd ?? cleanupBrowserSessionsForLifecycleEnd)({
sessionKeys: [entry.childSessionKey],
onWarn: (msg) => params.warn(msg, { runId: entry.runId }),
});

View File

@@ -1,3 +1,4 @@
import { cleanupBrowserSessionsForLifecycleEnd } from "../browser-lifecycle-cleanup.js";
import { loadConfig } from "../config/config.js";
import type { ensureContextEnginesInitialized as ensureContextEnginesInitializedFn } from "../context-engine/init.js";
import type { resolveContextEngine as resolveContextEngineFn } from "../context-engine/registry.js";
@@ -64,6 +65,7 @@ const log = createSubsystemLogger("agents/subagent-registry");
type SubagentRegistryDeps = {
callGateway: typeof callGateway;
captureSubagentCompletionReply: typeof subagentAnnounceModule.captureSubagentCompletionReply;
cleanupBrowserSessionsForLifecycleEnd: typeof cleanupBrowserSessionsForLifecycleEnd;
getSubagentRunsSnapshotForRead: typeof getSubagentRunsSnapshotForRead;
loadConfig: typeof loadConfig;
onAgentEvent: typeof onAgentEvent;
@@ -80,6 +82,7 @@ const defaultSubagentRegistryDeps: SubagentRegistryDeps = {
callGateway,
captureSubagentCompletionReply: (sessionKey) =>
subagentAnnounceModule.captureSubagentCompletionReply(sessionKey),
cleanupBrowserSessionsForLifecycleEnd,
getSubagentRunsSnapshotForRead,
loadConfig,
onAgentEvent,
@@ -313,6 +316,8 @@ const subagentLifecycleController = createSubagentRegistryLifecycleController({
resumeSubagentRun,
captureSubagentCompletionReply: (sessionKey) =>
subagentRegistryDeps.captureSubagentCompletionReply(sessionKey),
cleanupBrowserSessionsForLifecycleEnd: (args) =>
subagentRegistryDeps.cleanupBrowserSessionsForLifecycleEnd(args),
runSubagentAnnounceFlow: (params) => subagentRegistryDeps.runSubagentAnnounceFlow(params),
warn: (message, meta) => log.warn(message, meta),
});

View File

@@ -15,7 +15,7 @@ export { resolveAgentConfig } from "./agent-scope.js";
export { AGENT_LANE_SUBAGENT } from "./lanes.js";
export { resolveSubagentSpawnModelSelection } from "./model-selection.js";
export { resolveSandboxRuntimeStatus } from "./sandbox/runtime-status.js";
export { buildSubagentSystemPrompt } from "./subagent-announce.js";
export { buildSubagentSystemPrompt } from "./subagent-system-prompt.js";
export {
resolveDisplaySessionKey,
resolveInternalSessionKey,

View File

@@ -0,0 +1,112 @@
import { DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH } from "../config/agent-limits.js";
import type { DeliveryContext } from "../utils/delivery-context.js";
export function buildSubagentSystemPrompt(params: {
requesterSessionKey?: string;
requesterOrigin?: DeliveryContext;
childSessionKey: string;
label?: string;
task?: string;
/** Whether ACP-specific routing guidance should be included. Defaults to true. */
acpEnabled?: boolean;
/** Depth of the child being spawned (1 = sub-agent, 2 = sub-sub-agent). */
childDepth?: number;
/** Config value: max allowed spawn depth. */
maxSpawnDepth?: number;
}) {
const taskText =
typeof params.task === "string" && params.task.trim()
? params.task.replace(/\s+/g, " ").trim()
: "{{TASK_DESCRIPTION}}";
const childDepth = typeof params.childDepth === "number" ? params.childDepth : 1;
const maxSpawnDepth =
typeof params.maxSpawnDepth === "number"
? params.maxSpawnDepth
: DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH;
const acpEnabled = params.acpEnabled !== false;
const canSpawn = childDepth < maxSpawnDepth;
const parentLabel = childDepth >= 2 ? "parent orchestrator" : "main agent";
const lines = [
"# Subagent Context",
"",
`You are a **subagent** spawned by the ${parentLabel} for a specific task.`,
"",
"## Your Role",
`- You were created to handle: ${taskText}`,
"- Complete this task. That's your entire purpose.",
`- You are NOT the ${parentLabel}. Don't try to be.`,
"",
"## Rules",
"1. **Stay focused** - Do your assigned task, nothing else",
`2. **Complete the task** - Your final message will be automatically reported to the ${parentLabel}`,
"3. **Don't initiate** - No heartbeats, no proactive actions, no side quests",
"4. **Be ephemeral** - You may be terminated after task completion. That's fine.",
"5. **Trust push-based completion** - Descendant results are auto-announced back to you; do not busy-poll for status.",
"6. **Recover from truncated tool output** - If you see a notice like `[... N more characters truncated]`, assume prior output was reduced. Re-read only what you need using smaller chunks (`read` with offset/limit, or targeted `rg`/`head`/`tail`) instead of full-file `cat`.",
"",
"## Output Format",
"When complete, your final response should include:",
"- What you accomplished or found",
`- Any relevant details the ${parentLabel} should know`,
"- Keep it concise but informative",
"",
"## What You DON'T Do",
`- NO user conversations (that's ${parentLabel}'s job)`,
"- NO external messages (email, tweets, etc.) unless explicitly tasked with a specific recipient/channel",
"- NO cron jobs or persistent state",
`- NO pretending to be the ${parentLabel}`,
`- Only use the \`message\` tool when explicitly instructed to contact a specific external recipient; otherwise return plain text and let the ${parentLabel} deliver it`,
"",
];
if (canSpawn) {
lines.push(
"## Sub-Agent Spawning",
"You CAN spawn your own sub-agents for parallel or complex work using `sessions_spawn`.",
"Use the `subagents` tool to steer, kill, or do an on-demand status check for your spawned sub-agents.",
"Your sub-agents will announce their results back to you automatically (not to the main agent).",
"Default workflow: spawn work, continue orchestrating, and wait for auto-announced completions.",
"Auto-announce is push-based. After spawning children, do NOT call sessions_list, sessions_history, exec sleep, or any polling tool.",
"Wait for completion events to arrive as user messages.",
"Track expected child session keys and only send your final answer after completion events for ALL expected children arrive.",
"If a child completion event arrives AFTER you already sent your final answer, reply ONLY with NO_REPLY.",
"Do NOT repeatedly poll `subagents list` in a loop unless you are actively debugging or intervening.",
"Coordinate their work and synthesize results before reporting back.",
...(acpEnabled
? [
'For ACP harness sessions (codex/claudecode/gemini), use `sessions_spawn` with `runtime: "acp"` (set `agentId` unless `acp.defaultAgent` is configured).',
'`agents_list` and `subagents` apply to OpenClaw sub-agents (`runtime: "subagent"`); ACP harness ids are controlled by `acp.allowedAgents`.',
"Do not ask users to run slash commands or CLI when `sessions_spawn` can do it directly.",
"Do not use `exec` (`openclaw ...`, `acpx ...`) to spawn ACP sessions.",
'Use `subagents` only for OpenClaw subagents (`runtime: "subagent"`).',
"Subagent results auto-announce back to you; ACP sessions continue in their bound thread.",
"Avoid polling loops; spawn, orchestrate, and synthesize results.",
]
: []),
"",
);
} else if (childDepth >= 2) {
lines.push(
"## Sub-Agent Spawning",
"You are a leaf worker and CANNOT spawn further sub-agents. Focus on your assigned task.",
"",
);
}
lines.push(
"## Session Context",
...[
params.label ? `- Label: ${params.label}` : undefined,
params.requesterSessionKey
? `- Requester session: ${params.requesterSessionKey}.`
: undefined,
params.requesterOrigin?.channel
? `- Requester channel: ${params.requesterOrigin.channel}.`
: undefined,
`- Your session: ${params.childSessionKey}.`,
].filter((line): line is string => line !== undefined),
"",
);
return lines.join("\n");
}

View File

@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import { typedCases } from "../test-utils/typed-cases.js";
import { buildSubagentSystemPrompt } from "./subagent-announce.js";
import { buildSubagentSystemPrompt } from "./subagent-system-prompt.js";
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "./system-prompt-cache-boundary.js";
import { buildAgentSystemPrompt, buildRuntimeLine } from "./system-prompt.js";

View File

@@ -3,7 +3,6 @@ import { loadConfig } from "../../config/config.js";
import { callGateway } from "../../gateway/call.js";
import { normalizeDeliveryContext } from "../../utils/delivery-context.js";
import type { GatewayMessageChannel } from "../../utils/message-channel.js";
import { isSpawnAcpAcceptedResult, spawnAcpDirect } from "../acp-spawn.js";
import { optionalStringEnum } from "../schema/typebox.js";
import type { SpawnedToolContext } from "../spawned-context.js";
import { registerSubagentRun } from "../subagent-registry.js";
@@ -35,6 +34,15 @@ const UNSUPPORTED_SESSIONS_SPAWN_PARAM_KEYS = [
"reply_to",
] as const;
type AcpSpawnModule = typeof import("../acp-spawn.js");
let acpSpawnModulePromise: Promise<AcpSpawnModule> | undefined;
async function loadAcpSpawnModule(): Promise<AcpSpawnModule> {
acpSpawnModulePromise ??= import("../acp-spawn.js");
return await acpSpawnModulePromise;
}
function summarizeError(err: unknown): string {
if (err instanceof Error) {
return err.message;
@@ -207,6 +215,7 @@ export function createSessionsSpawnTool(
}
if (runtime === "acp") {
const { isSpawnAcpAcceptedResult, spawnAcpDirect } = await loadAcpSpawnModule();
if (Array.isArray(attachments) && attachments.length > 0) {
return jsonResult({
status: "error",

View File

@@ -1,9 +1,18 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { beforeAll, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
vi.unmock("../plugins/provider-runtime.js");
vi.unmock("../plugins/provider-runtime.runtime.js");
vi.unmock("../plugins/providers.runtime.js");
vi.mock("../plugins/provider-runtime.js", () => ({
resolveProviderRuntimePlugin: vi.fn(({ provider }: { provider?: string }) =>
provider === "mistral"
? {
buildReplayPolicy: () => ({
sanitizeToolCallIds: true,
toolCallIdMode: "strict9",
}),
}
: undefined,
),
}));
let resolveTranscriptPolicy: typeof import("./transcript-policy.js").resolveTranscriptPolicy;
const MISTRAL_PLUGIN_CONFIG = {
@@ -32,15 +41,11 @@ function createProviderRuntimeSmokeContext(): {
};
}
beforeEach(async () => {
vi.resetModules();
vi.doUnmock("../plugins/provider-runtime.js");
vi.doUnmock("../plugins/provider-runtime.runtime.js");
vi.doUnmock("../plugins/providers.runtime.js");
beforeAll(async () => {
({ resolveTranscriptPolicy } = await import("./transcript-policy.js"));
});
describe("resolveTranscriptPolicy e2e smoke", () => {
describe("resolveTranscriptPolicy provider replay policy", () => {
it("uses images-only sanitization without tool-call id rewriting for OpenAI models", () => {
const policy = resolveTranscriptPolicy({
...createProviderRuntimeSmokeContext(),