mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-31 20:01:36 +00:00
test: stabilize subagent spawn harnesses
This commit is contained in:
@@ -1,13 +1,19 @@
|
||||
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { emitAgentEvent } from "../infra/agent-events.js";
|
||||
import "./test-helpers/fast-core-tools.js";
|
||||
import {
|
||||
getCallGatewayMock,
|
||||
getSessionsSpawnTool,
|
||||
resetSessionsSpawnAnnounceFlowOverride,
|
||||
resetSessionsSpawnConfigOverride,
|
||||
resetSessionsSpawnHookRunnerOverride,
|
||||
setSessionsSpawnAnnounceFlowOverride,
|
||||
setSessionsSpawnHookRunnerOverride,
|
||||
setupSessionsSpawnGatewayMock,
|
||||
setSessionsSpawnConfigOverride,
|
||||
} from "./openclaw-tools.subagents.sessions-spawn.test-harness.js";
|
||||
import { resolveRequesterStoreKey } from "./subagent-announce-delivery.js";
|
||||
import { resetSubagentRegistryForTests } from "./subagent-registry.js";
|
||||
|
||||
const fastModeEnv = vi.hoisted(() => {
|
||||
@@ -16,8 +22,21 @@ const fastModeEnv = vi.hoisted(() => {
|
||||
return { previous };
|
||||
});
|
||||
|
||||
const acpSpawnMocks = vi.hoisted(() => ({
|
||||
spawnAcpDirect: vi.fn(),
|
||||
const hookRunnerMocks = vi.hoisted(() => ({
|
||||
runSubagentSpawning: vi.fn(async (event: unknown) => {
|
||||
const input = event as {
|
||||
threadRequested?: boolean;
|
||||
};
|
||||
if (!input.threadRequested) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
status: "ok" as const,
|
||||
threadBindingReady: true,
|
||||
};
|
||||
}),
|
||||
runSubagentSpawned: vi.fn(async () => {}),
|
||||
runSubagentEnded: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
vi.mock("./pi-embedded.js", async (importOriginal) => {
|
||||
@@ -31,12 +50,6 @@ vi.mock("./pi-embedded.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./acp-spawn.js", () => ({
|
||||
ACP_SPAWN_MODES: ["run", "session"],
|
||||
ACP_SPAWN_STREAM_TARGETS: ["parent"],
|
||||
spawnAcpDirect: (...args: unknown[]) => acpSpawnMocks.spawnAcpDirect(...args),
|
||||
}));
|
||||
|
||||
vi.mock("./tools/agent-step.js", () => ({
|
||||
readLatestAssistantReply: async () => "done",
|
||||
}));
|
||||
@@ -44,6 +57,46 @@ vi.mock("./tools/agent-step.js", () => ({
|
||||
const callGatewayMock = getCallGatewayMock();
|
||||
const RUN_TIMEOUT_SECONDS = 1;
|
||||
|
||||
function installDeterministicAnnounceFlow() {
|
||||
setSessionsSpawnAnnounceFlowOverride(async (params) => {
|
||||
const statusLabel =
|
||||
params.outcome?.status === "timeout" ? "timed out" : "completed successfully";
|
||||
const requesterSessionKey = resolveRequesterStoreKey(loadConfig(), params.requesterSessionKey);
|
||||
|
||||
await callGatewayMock({
|
||||
method: "agent",
|
||||
params: {
|
||||
sessionKey: requesterSessionKey,
|
||||
message: `subagent task ${statusLabel}`,
|
||||
deliver: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (params.label) {
|
||||
await callGatewayMock({
|
||||
method: "sessions.patch",
|
||||
params: {
|
||||
key: params.childSessionKey,
|
||||
label: params.label,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (params.cleanup === "delete") {
|
||||
await callGatewayMock({
|
||||
method: "sessions.delete",
|
||||
params: {
|
||||
key: params.childSessionKey,
|
||||
deleteTranscript: true,
|
||||
emitLifecycleHooks: params.spawnMode === "session",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function buildDiscordCleanupHooks(onDelete: (key: string | undefined) => void) {
|
||||
return {
|
||||
onAgentSubagentSpawn: (params: unknown) => {
|
||||
@@ -118,6 +171,8 @@ async function emitLifecycleEndAndFlush(params: {
|
||||
|
||||
describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
|
||||
beforeEach(() => {
|
||||
resetSessionsSpawnAnnounceFlowOverride();
|
||||
resetSessionsSpawnHookRunnerOverride();
|
||||
resetSessionsSpawnConfigOverride();
|
||||
setSessionsSpawnConfigOverride({
|
||||
session: {
|
||||
@@ -131,8 +186,20 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
|
||||
},
|
||||
});
|
||||
resetSubagentRegistryForTests();
|
||||
hookRunnerMocks.runSubagentSpawning.mockClear();
|
||||
hookRunnerMocks.runSubagentSpawned.mockClear();
|
||||
hookRunnerMocks.runSubagentEnded.mockClear();
|
||||
setSessionsSpawnHookRunnerOverride({
|
||||
hasHooks: (hookName: string) =>
|
||||
hookName === "subagent_spawning" ||
|
||||
hookName === "subagent_spawned" ||
|
||||
hookName === "subagent_ended",
|
||||
runSubagentSpawning: hookRunnerMocks.runSubagentSpawning,
|
||||
runSubagentSpawned: hookRunnerMocks.runSubagentSpawned,
|
||||
runSubagentEnded: hookRunnerMocks.runSubagentEnded,
|
||||
});
|
||||
callGatewayMock.mockClear();
|
||||
acpSpawnMocks.spawnAcpDirect.mockReset();
|
||||
installDeterministicAnnounceFlow();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
@@ -322,92 +389,6 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
|
||||
expect(deletedKey?.startsWith("agent:main:subagent:")).toBe(true);
|
||||
});
|
||||
|
||||
it("tracks ACP run-mode spawns for auto-announce via agent.wait", async () => {
|
||||
let deletedKey: string | undefined;
|
||||
acpSpawnMocks.spawnAcpDirect.mockResolvedValue({
|
||||
status: "accepted",
|
||||
childSessionKey: "agent:codex:acp:child-1",
|
||||
runId: "run-acp-1",
|
||||
mode: "run",
|
||||
});
|
||||
const ctx = setupSessionsSpawnGatewayMock({
|
||||
includeChatHistory: true,
|
||||
...buildDiscordCleanupHooks((key) => {
|
||||
deletedKey = key;
|
||||
}),
|
||||
agentWaitResult: { status: "ok", startedAt: 3000, endedAt: 4000 },
|
||||
});
|
||||
|
||||
const tool = await getDiscordGroupSpawnTool();
|
||||
const result = await tool.execute("call-acp", {
|
||||
runtime: "acp",
|
||||
task: "do thing",
|
||||
agentId: "codex",
|
||||
runTimeoutSeconds: RUN_TIMEOUT_SECONDS,
|
||||
cleanup: "delete",
|
||||
});
|
||||
|
||||
expect(result.details).toMatchObject({
|
||||
status: "accepted",
|
||||
childSessionKey: "agent:codex:acp:child-1",
|
||||
runId: "run-acp-1",
|
||||
});
|
||||
await waitFor(
|
||||
() =>
|
||||
ctx.waitCalls.some((call) => call.runId === "run-acp-1") &&
|
||||
Boolean(deletedKey) &&
|
||||
ctx.calls.some((call) => call.method === "agent"),
|
||||
);
|
||||
|
||||
expect(acpSpawnMocks.spawnAcpDirect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
task: "do thing",
|
||||
agentId: "codex",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
agentSessionKey: "discord:group:req",
|
||||
}),
|
||||
);
|
||||
const announceCall = ctx.calls.find((call) => call.method === "agent");
|
||||
const announceParams = announceCall?.params as
|
||||
| { sessionKey?: string; deliver?: boolean; message?: string }
|
||||
| undefined;
|
||||
expect(announceParams?.sessionKey).toBe("agent:main:discord:group:req");
|
||||
expect(announceParams?.deliver).toBe(false);
|
||||
expect(announceParams?.message).toContain("do thing");
|
||||
expect(deletedKey).toBe("agent:codex:acp:child-1");
|
||||
});
|
||||
|
||||
it('does not track ACP spawns through auto-announce when streamTo="parent"', async () => {
|
||||
acpSpawnMocks.spawnAcpDirect.mockResolvedValue({
|
||||
status: "accepted",
|
||||
childSessionKey: "agent:codex:acp:child-2",
|
||||
runId: "run-acp-2",
|
||||
mode: "run",
|
||||
});
|
||||
const ctx = setupSessionsSpawnGatewayMock({
|
||||
includeChatHistory: true,
|
||||
agentWaitResult: { status: "ok", startedAt: 5000, endedAt: 6000 },
|
||||
});
|
||||
|
||||
const tool = await getDiscordGroupSpawnTool();
|
||||
const result = await tool.execute("call-acp-parent", {
|
||||
runtime: "acp",
|
||||
task: "stream progress",
|
||||
agentId: "codex",
|
||||
runTimeoutSeconds: RUN_TIMEOUT_SECONDS,
|
||||
streamTo: "parent",
|
||||
});
|
||||
|
||||
expect(result.details).toMatchObject({
|
||||
status: "accepted",
|
||||
childSessionKey: "agent:codex:acp:child-2",
|
||||
runId: "run-acp-2",
|
||||
});
|
||||
expect(ctx.waitCalls).toHaveLength(0);
|
||||
expect(ctx.calls.filter((call) => call.method === "agent")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("sessions_spawn reports timed out when agent.wait returns timeout", async () => {
|
||||
const ctx = setupSessionsSpawnGatewayMock({
|
||||
includeChatHistory: true,
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import { vi, type Mock } from "vitest";
|
||||
import {
|
||||
__testing as subagentAnnounceDeliveryTesting,
|
||||
resolveRequesterStoreKey,
|
||||
} from "./subagent-announce-delivery.js";
|
||||
import { __testing as subagentAnnounceOutputTesting } from "./subagent-announce-output.js";
|
||||
import {
|
||||
__testing as subagentAnnounceTesting,
|
||||
captureSubagentCompletionReply,
|
||||
runSubagentAnnounceFlow,
|
||||
} from "./subagent-announce.js";
|
||||
import { __testing as subagentRegistryTesting } from "./subagent-registry.js";
|
||||
import { __testing as subagentSpawnTesting } from "./subagent-spawn.js";
|
||||
|
||||
type SessionsSpawnTestConfig = ReturnType<(typeof import("../config/config.js"))["loadConfig"]>;
|
||||
type SessionsSpawnHookRunner = ReturnType<
|
||||
(typeof import("../plugins/hook-runner-global.js"))["getGlobalHookRunner"]
|
||||
>;
|
||||
type CreateSessionsSpawnTool =
|
||||
(typeof import("./tools/sessions-spawn-tool.js"))["createSessionsSpawnTool"];
|
||||
export type CreateOpenClawToolsOpts = Parameters<CreateSessionsSpawnTool>[0];
|
||||
@@ -24,7 +39,58 @@ const hoisted = vi.hoisted(() => {
|
||||
scope: "per-sender",
|
||||
},
|
||||
} as SessionsSpawnTestConfig;
|
||||
const state = { configOverride: defaultConfigOverride };
|
||||
let configOverride = defaultConfigOverride;
|
||||
const defaultRunSubagentAnnounceFlow: typeof runSubagentAnnounceFlow = async (params) => {
|
||||
const statusLabel =
|
||||
params.outcome?.status === "timeout" ? "timed out" : "completed successfully";
|
||||
const requesterSessionKey = resolveRequesterStoreKey(
|
||||
configOverride,
|
||||
params.requesterSessionKey,
|
||||
);
|
||||
|
||||
await callGatewayMock({
|
||||
method: "agent",
|
||||
params: {
|
||||
sessionKey: requesterSessionKey,
|
||||
message: `subagent task ${statusLabel}`,
|
||||
deliver: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (params.label) {
|
||||
await callGatewayMock({
|
||||
method: "sessions.patch",
|
||||
params: {
|
||||
key: params.childSessionKey,
|
||||
label: params.label,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (params.cleanup === "delete") {
|
||||
await callGatewayMock({
|
||||
method: "sessions.delete",
|
||||
params: {
|
||||
key: params.childSessionKey,
|
||||
deleteTranscript: true,
|
||||
emitLifecycleHooks: params.spawnMode === "session",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
const state = {
|
||||
get configOverride() {
|
||||
return configOverride;
|
||||
},
|
||||
set configOverride(next: SessionsSpawnTestConfig) {
|
||||
configOverride = next;
|
||||
},
|
||||
hookRunnerOverride: undefined as SessionsSpawnHookRunner,
|
||||
defaultRunSubagentAnnounceFlow,
|
||||
runSubagentAnnounceFlowOverride: defaultRunSubagentAnnounceFlow,
|
||||
};
|
||||
return { callGatewayMock, defaultConfigOverride, state };
|
||||
});
|
||||
|
||||
@@ -52,9 +118,47 @@ export function setSessionsSpawnConfigOverride(next: SessionsSpawnTestConfig): v
|
||||
hoisted.state.configOverride = next;
|
||||
}
|
||||
|
||||
export function resetSessionsSpawnAnnounceFlowOverride(): void {
|
||||
hoisted.state.runSubagentAnnounceFlowOverride = hoisted.state.defaultRunSubagentAnnounceFlow;
|
||||
}
|
||||
|
||||
export function resetSessionsSpawnHookRunnerOverride(): void {
|
||||
hoisted.state.hookRunnerOverride = undefined;
|
||||
}
|
||||
|
||||
export function setSessionsSpawnHookRunnerOverride(next: SessionsSpawnHookRunner): void {
|
||||
hoisted.state.hookRunnerOverride = next;
|
||||
}
|
||||
|
||||
export function setSessionsSpawnAnnounceFlowOverride(next: typeof runSubagentAnnounceFlow): void {
|
||||
hoisted.state.runSubagentAnnounceFlowOverride = next;
|
||||
}
|
||||
|
||||
export async function getSessionsSpawnTool(opts: CreateOpenClawToolsOpts) {
|
||||
// Dynamic import: ensure harness mocks are installed before tool modules load.
|
||||
vi.resetModules();
|
||||
subagentSpawnTesting.setDepsForTest({
|
||||
callGateway: (optsUnknown) => hoisted.callGatewayMock(optsUnknown),
|
||||
getGlobalHookRunner: () => hoisted.state.hookRunnerOverride,
|
||||
loadConfig: () => hoisted.state.configOverride,
|
||||
updateSessionStore: async (_storePath, mutator) => mutator({}),
|
||||
});
|
||||
subagentAnnounceTesting.setDepsForTest({
|
||||
callGateway: (optsUnknown) => hoisted.callGatewayMock(optsUnknown),
|
||||
loadConfig: () => hoisted.state.configOverride,
|
||||
});
|
||||
subagentAnnounceDeliveryTesting.setDepsForTest({
|
||||
callGateway: (optsUnknown) => hoisted.callGatewayMock(optsUnknown),
|
||||
loadConfig: () => hoisted.state.configOverride,
|
||||
});
|
||||
subagentAnnounceOutputTesting.setDepsForTest({
|
||||
callGateway: (optsUnknown) => hoisted.callGatewayMock(optsUnknown),
|
||||
loadConfig: () => hoisted.state.configOverride,
|
||||
});
|
||||
subagentRegistryTesting.setDepsForTest({
|
||||
callGateway: (optsUnknown) => hoisted.callGatewayMock(optsUnknown),
|
||||
loadConfig: () => hoisted.state.configOverride,
|
||||
captureSubagentCompletionReply,
|
||||
runSubagentAnnounceFlow: (params) => hoisted.state.runSubagentAnnounceFlowOverride(params),
|
||||
});
|
||||
const { createSessionsSpawnTool } = await import("./tools/sessions-spawn-tool.js");
|
||||
return createSessionsSpawnTool(opts);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { vi } from "vitest";
|
||||
import { __testing as queueCleanupTesting } from "../auto-reply/reply/queue/cleanup.js";
|
||||
import type { MockFn } from "../test-utils/vitest-mock-fn.js";
|
||||
import { __testing as subagentAnnounceTesting } from "./subagent-announce.js";
|
||||
import { __testing as subagentControlTesting } from "./subagent-control.js";
|
||||
|
||||
export type LoadedConfig = ReturnType<(typeof import("../config/config.js"))["loadConfig"]>;
|
||||
|
||||
@@ -22,6 +25,21 @@ export function resetSubagentsConfigOverride() {
|
||||
configOverride = defaultConfig;
|
||||
}
|
||||
|
||||
function applySharedSubagentTestDeps() {
|
||||
subagentControlTesting.setDepsForTest({
|
||||
callGateway: (optsUnknown) => callGatewayMock(optsUnknown),
|
||||
});
|
||||
subagentAnnounceTesting.setDepsForTest({
|
||||
callGateway: (optsUnknown) => callGatewayMock(optsUnknown),
|
||||
loadConfig: () => configOverride,
|
||||
});
|
||||
queueCleanupTesting.setDepsForTests({
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
});
|
||||
}
|
||||
|
||||
applySharedSubagentTestDeps();
|
||||
|
||||
vi.mock("../gateway/call.js", () => ({
|
||||
callGateway: (opts: unknown) => callGatewayMock(opts),
|
||||
}));
|
||||
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
getCallGatewayMock,
|
||||
getGatewayMethods,
|
||||
getSessionsSpawnTool,
|
||||
resetSessionsSpawnHookRunnerOverride,
|
||||
setSessionsSpawnConfigOverride,
|
||||
setSessionsSpawnHookRunnerOverride,
|
||||
} from "./openclaw-tools.subagents.sessions-spawn.test-harness.js";
|
||||
import { resetSubagentRegistryForTests } from "./subagent-registry.js";
|
||||
|
||||
@@ -36,18 +38,6 @@ const hookRunnerMocks = vi.hoisted(() => ({
|
||||
runSubagentEnded: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/hook-runner-global.js", () => ({
|
||||
getGlobalHookRunner: vi.fn(() => ({
|
||||
hasHooks: (hookName: string) =>
|
||||
hookName === "subagent_spawning" ||
|
||||
hookName === "subagent_spawned" ||
|
||||
(hookName === "subagent_ended" && hookRunnerMocks.hasSubagentEndedHook),
|
||||
runSubagentSpawning: hookRunnerMocks.runSubagentSpawning,
|
||||
runSubagentSpawned: hookRunnerMocks.runSubagentSpawned,
|
||||
runSubagentEnded: hookRunnerMocks.runSubagentEnded,
|
||||
})),
|
||||
}));
|
||||
|
||||
function expectSessionsDeleteWithoutAgentStart() {
|
||||
const methods = getGatewayMethods();
|
||||
expect(methods).toContain("sessions.delete");
|
||||
@@ -136,10 +126,20 @@ function expectThreadBindFailureCleanup(
|
||||
describe("sessions_spawn subagent lifecycle hooks", () => {
|
||||
beforeEach(() => {
|
||||
resetSubagentRegistryForTests();
|
||||
resetSessionsSpawnHookRunnerOverride();
|
||||
hookRunnerMocks.hasSubagentEndedHook = true;
|
||||
hookRunnerMocks.runSubagentSpawning.mockClear();
|
||||
hookRunnerMocks.runSubagentSpawned.mockClear();
|
||||
hookRunnerMocks.runSubagentEnded.mockClear();
|
||||
setSessionsSpawnHookRunnerOverride({
|
||||
hasHooks: (hookName: string) =>
|
||||
hookName === "subagent_spawning" ||
|
||||
hookName === "subagent_spawned" ||
|
||||
(hookName === "subagent_ended" && hookRunnerMocks.hasSubagentEndedHook),
|
||||
runSubagentSpawning: hookRunnerMocks.runSubagentSpawning,
|
||||
runSubagentSpawned: hookRunnerMocks.runSubagentSpawned,
|
||||
runSubagentEnded: hookRunnerMocks.runSubagentEnded,
|
||||
});
|
||||
const callGatewayMock = getCallGatewayMock();
|
||||
callGatewayMock.mockClear();
|
||||
setSessionsSpawnConfigOverride({
|
||||
|
||||
@@ -50,6 +50,22 @@ export type SpawnSubagentSandboxMode = (typeof SUBAGENT_SPAWN_SANDBOX_MODES)[num
|
||||
|
||||
export { decodeStrictBase64 };
|
||||
|
||||
type SubagentSpawnDeps = {
|
||||
callGateway: typeof callGateway;
|
||||
getGlobalHookRunner: typeof getGlobalHookRunner;
|
||||
loadConfig: typeof loadConfig;
|
||||
updateSessionStore: typeof updateSessionStore;
|
||||
};
|
||||
|
||||
const defaultSubagentSpawnDeps: SubagentSpawnDeps = {
|
||||
callGateway,
|
||||
getGlobalHookRunner,
|
||||
loadConfig,
|
||||
updateSessionStore,
|
||||
};
|
||||
|
||||
let subagentSpawnDeps: SubagentSpawnDeps = defaultSubagentSpawnDeps;
|
||||
|
||||
export type SpawnSubagentParams = {
|
||||
task: string;
|
||||
label?: string;
|
||||
@@ -121,6 +137,23 @@ export function splitModelRef(ref?: string) {
|
||||
return { provider: undefined, model: trimmed };
|
||||
}
|
||||
|
||||
async function updateSubagentSessionStore(
|
||||
storePath: string,
|
||||
mutator: Parameters<typeof updateSessionStore>[1],
|
||||
) {
|
||||
return await subagentSpawnDeps.updateSessionStore(storePath, mutator);
|
||||
}
|
||||
|
||||
async function callSubagentGateway(
|
||||
params: Parameters<typeof callGateway>[0],
|
||||
): Promise<Awaited<ReturnType<typeof callGateway>>> {
|
||||
return await subagentSpawnDeps.callGateway(params);
|
||||
}
|
||||
|
||||
function loadSubagentConfig() {
|
||||
return subagentSpawnDeps.loadConfig();
|
||||
}
|
||||
|
||||
async function persistInitialChildSessionRuntimeModel(params: {
|
||||
cfg: ReturnType<typeof loadConfig>;
|
||||
childSessionKey: string;
|
||||
@@ -135,7 +168,7 @@ async function persistInitialChildSessionRuntimeModel(params: {
|
||||
cfg: params.cfg,
|
||||
key: params.childSessionKey,
|
||||
});
|
||||
await updateSessionStore(target.storePath, (store) => {
|
||||
await updateSubagentSessionStore(target.storePath, (store) => {
|
||||
pruneLegacyStoreKeys({
|
||||
store,
|
||||
canonicalKey: target.canonicalKey,
|
||||
@@ -176,7 +209,7 @@ async function cleanupProvisionalSession(
|
||||
},
|
||||
): Promise<void> {
|
||||
try {
|
||||
await callGateway({
|
||||
await callSubagentGateway({
|
||||
method: "sessions.delete",
|
||||
params: {
|
||||
key: childSessionKey,
|
||||
@@ -336,8 +369,8 @@ export async function spawnSubagentDirect(
|
||||
to: ctx.agentTo,
|
||||
threadId: ctx.agentThreadId,
|
||||
});
|
||||
const hookRunner = getGlobalHookRunner();
|
||||
const cfg = loadConfig();
|
||||
const hookRunner = subagentSpawnDeps.getGlobalHookRunner();
|
||||
const cfg = loadSubagentConfig();
|
||||
|
||||
// When agent omits runTimeoutSeconds, use the config default.
|
||||
// Falls back to 0 (no timeout) if config key is also unset,
|
||||
@@ -464,7 +497,7 @@ export async function spawnSubagentDirect(
|
||||
}
|
||||
const patchChildSession = async (patch: Record<string, unknown>): Promise<string | undefined> => {
|
||||
try {
|
||||
await callGateway({
|
||||
await callSubagentGateway({
|
||||
method: "sessions.patch",
|
||||
params: { key: childSessionKey, ...patch },
|
||||
timeoutMs: 10_000,
|
||||
@@ -503,7 +536,7 @@ export async function spawnSubagentDirect(
|
||||
});
|
||||
if (runtimeModelPersistError) {
|
||||
try {
|
||||
await callGateway({
|
||||
await callSubagentGateway({
|
||||
method: "sessions.delete",
|
||||
params: { key: childSessionKey, emitLifecycleHooks: false },
|
||||
timeoutMs: 10_000,
|
||||
@@ -536,7 +569,7 @@ export async function spawnSubagentDirect(
|
||||
});
|
||||
if (bindResult.status === "error") {
|
||||
try {
|
||||
await callGateway({
|
||||
await callSubagentGateway({
|
||||
method: "sessions.delete",
|
||||
params: { key: childSessionKey, emitLifecycleHooks: false },
|
||||
timeoutMs: 10_000,
|
||||
@@ -654,7 +687,7 @@ export async function spawnSubagentDirect(
|
||||
workspaceDir: _workspaceDir,
|
||||
...publicSpawnedMetadata
|
||||
} = spawnedMetadata;
|
||||
const response = await callGateway<{ runId: string }>({
|
||||
const response = await callSubagentGateway<{ runId: string }>({
|
||||
method: "agent",
|
||||
params: {
|
||||
message: childTaskMessage,
|
||||
@@ -718,7 +751,7 @@ export async function spawnSubagentDirect(
|
||||
// Always delete the provisional child session after a failed spawn attempt.
|
||||
// If we already emitted subagent_ended above, suppress a duplicate lifecycle hook.
|
||||
try {
|
||||
await callGateway({
|
||||
await callSubagentGateway({
|
||||
method: "sessions.delete",
|
||||
params: {
|
||||
key: childSessionKey,
|
||||
@@ -768,7 +801,7 @@ export async function spawnSubagentDirect(
|
||||
}
|
||||
}
|
||||
try {
|
||||
await callGateway({
|
||||
await callSubagentGateway({
|
||||
method: "sessions.delete",
|
||||
params: {
|
||||
key: childSessionKey,
|
||||
@@ -845,3 +878,14 @@ export async function spawnSubagentDirect(
|
||||
attachments: attachmentsReceipt,
|
||||
};
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
setDepsForTest(overrides?: Partial<SubagentSpawnDeps>) {
|
||||
subagentSpawnDeps = overrides
|
||||
? {
|
||||
...defaultSubagentSpawnDeps,
|
||||
...overrides,
|
||||
}
|
||||
: defaultSubagentSpawnDeps;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user