




















@@ -1,4 +1,5 @@
11import { randomBytes, randomUUID } from "node:crypto";
2+import fs from "node:fs/promises";
23import path from "node:path";
34import { afterEach, describe, expect, it } from "vitest";
45import { clearRuntimeConfigSnapshot, type OpenClawConfig } from "../config/config.js";
@@ -38,12 +39,90 @@ function sleep(ms: number): Promise<void> {
3839return new Promise((resolve) => setTimeout(resolve, ms));
3940}
404141-function openAiConfig(
42+type LiveSubagentModelConfig = {
43+modelKey: string;
44+provider: "openai" | "google";
45+requiredEnv: "OPENAI_API_KEY" | "GEMINI_API_KEY" | "GOOGLE_API_KEY";
46+};
47+type LiveSubagentModelProviders = NonNullable<NonNullable<OpenClawConfig["models"]>["providers"]>;
48+49+function resolveLiveSubagentModelConfig(): LiveSubagentModelConfig {
50+const modelKey = process.env.OPENCLAW_LIVE_SUBAGENT_E2E_MODEL?.trim() || "openai/gpt-5.5";
51+if (modelKey.startsWith("google/")) {
52+return {
53+ modelKey,
54+provider: "google",
55+requiredEnv: process.env.GEMINI_API_KEY?.trim() ? "GEMINI_API_KEY" : "GOOGLE_API_KEY",
56+};
57+}
58+return { modelKey, provider: "openai", requiredEnv: "OPENAI_API_KEY" };
59+}
60+61+function requireLiveSubagentAuth(config: LiveSubagentModelConfig): void {
62+expect(process.env[config.requiredEnv]?.trim(), config.requiredEnv).toBeTruthy();
63+}
64+65+function liveSubagentConfig(
4266modelKey: string,
4367workspace: string,
4468port: number,
4569token: string,
70+options?: { toolAllow?: string[] },
4671): OpenClawConfig {
72+const providerConfig = resolveLiveSubagentModelConfig();
73+const modelId = modelKey.replace(/^(openai|google)\//u, "");
74+const providers: LiveSubagentModelProviders = {};
75+if (providerConfig.provider === "google") {
76+providers.google = {
77+api: "google-generative-ai" as const,
78+agentRuntime: { id: "pi" },
79+baseUrl: "https://generativelanguage.googleapis.com/v1beta",
80+apiKey: {
81+source: "env" as const,
82+provider: "default" as const,
83+id: providerConfig.requiredEnv,
84+},
85+timeoutSeconds: 300,
86+models: [
87+{
88+id: modelId,
89+name: modelId,
90+api: "google-generative-ai" as const,
91+agentRuntime: { id: "pi" },
92+input: ["text" as const],
93+reasoning: true,
94+contextWindow: 1_048_576,
95+maxTokens: 8_192,
96+cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
97+},
98+],
99+};
100+} else {
101+providers.openai = {
102+api: "openai-responses" as const,
103+agentRuntime: { id: "pi" },
104+apiKey: {
105+source: "env" as const,
106+provider: "default" as const,
107+id: "OPENAI_API_KEY",
108+},
109+baseUrl: "https://api.openai.com/v1",
110+timeoutSeconds: 300,
111+models: [
112+{
113+id: modelId,
114+name: modelId,
115+api: "openai-responses" as const,
116+agentRuntime: { id: "pi" },
117+input: ["text" as const],
118+reasoning: true,
119+contextWindow: 1_047_576,
120+maxTokens: 8_192,
121+cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
122+},
123+],
124+};
125+}
47126return {
48127gateway: {
49128mode: "local",
@@ -52,32 +131,9 @@ function openAiConfig(
52131controlUi: { enabled: false },
53132},
54133plugins: { enabled: false },
55-tools: {
56-allow: ["sessions_spawn", "sessions_yield", "subagents"],
57-},
134+tools: { allow: options?.toolAllow ?? ["sessions_spawn", "sessions_yield", "subagents"] },
58135models: {
59-providers: {
60-openai: {
61-api: "openai-responses",
62-agentRuntime: { id: "pi" },
63-apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
64-baseUrl: "https://api.openai.com/v1",
65-timeoutSeconds: 300,
66-models: [
67-{
68-id: modelKey.replace(/^openai\//u, ""),
69-name: modelKey.replace(/^openai\//u, ""),
70-api: "openai-responses",
71-agentRuntime: { id: "pi" },
72-input: ["text"],
73-reasoning: true,
74-contextWindow: 1_047_576,
75-maxTokens: 8_192,
76-cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
77-},
78-],
79-},
80-},
136+ providers,
81137},
82138agents: {
83139defaults: {
@@ -155,11 +211,12 @@ describeLive("subagent announce live", () => {
155211it(
156212"lets a parent steer a subagent and receives completion through in-process agent dispatch",
157213async () => {
158-expect(process.env.OPENAI_API_KEY?.trim(), "OPENAI_API_KEY").toBeTruthy();
214+const modelConfig = resolveLiveSubagentModelConfig();
215+requireLiveSubagentAuth(modelConfig);
159216160217const token = `subagent-live-${randomUUID()}`;
161218const port = 30_000 + Math.floor(Math.random() * 10_000);
162-const modelKey = process.env.OPENCLAW_LIVE_SUBAGENT_E2E_MODEL?.trim() || "openai/gpt-5.5";
219+const modelKey = modelConfig.modelKey;
163220const nonce = randomBytes(3).toString("hex").toUpperCase();
164221const childToken = `CHILD_STEERED_${nonce}`;
165222const parentToken = `PARENT_SAW_${childToken}`;
@@ -226,7 +283,7 @@ describeLive("subagent announce live", () => {
226283OPENCLAW_PLUGINS_PATHS: undefined,
227284},
228285});
229-await state.writeConfig(openAiConfig(modelKey, state.workspaceDir, port, token));
286+await state.writeConfig(liveSubagentConfig(modelKey, state.workspaceDir, port, token));
230287clearRuntimeConfigSnapshot();
231288clearCurrentPluginMetadataSnapshot();
232289@@ -314,4 +371,146 @@ describeLive("subagent announce live", () => {
314371},
31537210 * 60_000,
316373);
374+375+it(
376+"runs parallel isolated Gemini subagents with tool-heavy schemas",
377+async () => {
378+const modelConfig = resolveLiveSubagentModelConfig();
379+if (!modelConfig.modelKey.startsWith("google/")) {
380+console.warn(
381+"[subagent-stress] skip: set OPENCLAW_LIVE_SUBAGENT_E2E_MODEL=google/gemini-3.1-pro-preview",
382+);
383+return;
384+}
385+requireLiveSubagentAuth(modelConfig);
386+387+const token = `subagent-stress-${randomUUID()}`;
388+const port = 30_000 + Math.floor(Math.random() * 10_000);
389+const nonce = randomBytes(3).toString("hex").toUpperCase();
390+const sessionKey = `agent:main:live-subagent-stress-${nonce.toLowerCase()}`;
391+const childTokens = [1, 2, 3].map((index) => `GEMINI_STRESS_${nonce}_${index}`);
392+const parentToken = `GEMINI_STRESS_PARENT_${nonce}`;
393+394+state = await createOpenClawTestState({
395+label: "subagent-gemini-stress-live",
396+layout: "split",
397+env: {
398+OPENCLAW_SKIP_CHANNELS: "1",
399+OPENCLAW_SKIP_CRON: "1",
400+OPENCLAW_SKIP_BROWSER_CONTROL_SERVER: "1",
401+OPENCLAW_SKIP_CANVAS_HOST: "1",
402+OPENCLAW_TEST_MINIMAL_GATEWAY: "1",
403+OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined,
404+OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY: "1",
405+OPENCLAW_BUNDLED_PLUGINS_DIR: path.resolve("extensions"),
406+OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR: "1",
407+OPENCLAW_PLUGIN_CATALOG_PATHS: undefined,
408+OPENCLAW_PLUGINS_PATHS: undefined,
409+OPENCLAW_DEBUG_MODEL_TRANSPORT: "1",
410+OPENCLAW_DEBUG_MODEL_PAYLOAD: "tools",
411+OPENCLAW_DEBUG_SSE: "events",
412+},
413+});
414+await fs.writeFile(
415+path.join(state.workspaceDir, "package.json"),
416+`${JSON.stringify({ name: "openclaw-gemini-stress-live", private: true }, null, 2)}\n`,
417+"utf8",
418+);
419+await fs.writeFile(
420+path.join(state.workspaceDir, "AGENTS.md"),
421+"OpenClaw live stress test workspace. Keep responses concise.\n",
422+"utf8",
423+);
424+await state.writeConfig(
425+liveSubagentConfig(modelConfig.modelKey, state.workspaceDir, port, token, {
426+toolAllow: [
427+"sessions_spawn",
428+"sessions_yield",
429+"subagents",
430+"bash",
431+"read",
432+"web_search",
433+"memory_search",
434+],
435+}),
436+);
437+clearRuntimeConfigSnapshot();
438+clearCurrentPluginMetadataSnapshot();
439+440+server = await startGatewayServer(port, {
441+bind: "loopback",
442+auth: { mode: "token", token },
443+controlUiEnabled: false,
444+});
445+client = await createGatewayClient({ port, token });
446+447+let initialError: unknown;
448+const initialRequest = client.request<AgentPayload>(
449+"agent",
450+{
451+ sessionKey,
452+idempotencyKey: `live-subagent-stress-${randomUUID()}`,
453+deliver: false,
454+timeout: 420,
455+message: [
456+"Run this exact OpenClaw Gemini subagent stress scenario. Use tool calls, not prose.",
457+`Use nonce ${nonce}.`,
458+"Spawn all three children before waiting for any child result.",
459+ ...childTokens.map((childToken, index) => {
460+const childNumber = index + 1;
461+return `Call sessions_spawn for child ${childNumber} with exactly this JSON input: ${JSON.stringify(
462+ {
463+ task: [
464+ `You are stress child ${childNumber}.`,
465+ "Use available tools for a tiny multi-tool check.",
466+ "First read package.json if the read tool is available.",
467+ "Then run a tiny shell command if the bash tool is available: printf openclaw.",
468+ "If web_search or memory_search is available, use at most one small query.",
469+ `After the tool work, reply exactly ${childToken}.`,
470+ ].join(" "),
471+ taskName: `gemini_stress_${childNumber}`,
472+ cleanup: "keep",
473+ context: "isolated",
474+ runTimeoutSeconds: 300,
475+ },
476+ )}.`;
477+}),
478+`After the three spawn calls are accepted, call sessions_yield with message="waiting for ${childTokens.join(
479+ ",",
480+ )}" and wait for all child completion events.`,
481+`Reply exactly ${parentToken} only after all three child tokens are visible.`,
482+].join("\n"),
483+},
484+{ expectFinal: true, timeoutMs: REQUEST_TIMEOUT_MS },
485+);
486+initialRequest.catch((error: unknown) => {
487+initialError = error;
488+});
489+490+const completedRuns = await waitFor("three Gemini stress child completions", () => {
491+if (initialError) {
492+throw initialError;
493+}
494+const runs = listSubagentRunsForRequester(sessionKey).filter((run) =>
495+run.taskName?.startsWith("gemini_stress_"),
496+);
497+const completed = childTokens.every((childToken) =>
498+runs.some(
499+(run) =>
500+run.frozenResultText?.includes(childToken) === true && run.outcome?.status === "ok",
501+),
502+);
503+return completed ? runs : undefined;
504+});
505+506+expect(completedRuns).toHaveLength(3);
507+for (const childToken of childTokens) {
508+expect(completedRuns.some((run) => run.frozenResultText?.includes(childToken))).toBe(true);
509+}
510+511+const parent = await initialRequest;
512+expect(extractPayloadText(parent.result)).toContain(parentToken);
513+},
514+12 * 60_000,
515+);
317516});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。