From e8817dde8e758d06e2d0ba50f9abcb6d438ac8d9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Apr 2026 07:13:56 +0100 Subject: [PATCH] perf(agents): remove spawn hook announce import tax --- ...subagents.sessions-spawn.lifecycle.test.ts | 2 +- ...s.subagents.sessions-spawn.test-harness.ts | 37 +- src/agents/sessions-spawn-hooks.test.ts | 347 ++++++++++-------- src/agents/subagent-announce-delivery.ts | 26 +- src/agents/subagent-requester-store-key.ts | 32 ++ src/agents/subagent-spawn.test-helpers.ts | 3 +- 6 files changed, 243 insertions(+), 204 deletions(-) create mode 100644 src/agents/subagent-requester-store-key.ts diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts index c39fe30530a..1deaed46dd8 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts @@ -13,11 +13,11 @@ import { setupSessionsSpawnGatewayMock, setSessionsSpawnConfigOverride, } from "./openclaw-tools.subagents.sessions-spawn.test-harness.js"; -import { resolveRequesterStoreKey } from "./subagent-announce-delivery.js"; import { getLatestSubagentRunByChildSessionKey, resetSubagentRegistryForTests, } from "./subagent-registry.js"; +import { resolveRequesterStoreKey } from "./subagent-requester-store-key.js"; const fastModeEnv = vi.hoisted(() => { const previous = process.env.OPENCLAW_TEST_FAST; diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn.test-harness.ts b/src/agents/openclaw-tools.subagents.sessions-spawn.test-harness.ts index a439bbc8196..4db4e80e6bc 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn.test-harness.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn.test-harness.ts @@ -1,20 +1,14 @@ import { vi, type Mock } from "vitest"; import type { SubagentLifecycleHookRunner } from "../plugins/hooks.js"; -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 { resolveRequesterStoreKey } from "./subagent-requester-store-key.js"; import { __testing as subagentSpawnTesting } from "./subagent-spawn.js"; type SessionsSpawnTestConfig = ReturnType<(typeof import("../config/config.js"))["loadConfig"]>; type SessionsSpawnHookRunner = SubagentLifecycleHookRunner | null; +type CaptureSubagentCompletionReply = + (typeof import("./subagent-announce.js"))["captureSubagentCompletionReply"]; +type RunSubagentAnnounceFlow = (typeof import("./subagent-announce.js"))["runSubagentAnnounceFlow"]; type CreateSessionsSpawnTool = (typeof import("./tools/sessions-spawn-tool.js"))["createSessionsSpawnTool"]; export type CreateOpenClawToolsOpts = Parameters[0]; @@ -39,7 +33,7 @@ const hoisted = vi.hoisted(() => { }, } as SessionsSpawnTestConfig; let configOverride = defaultConfigOverride; - const defaultRunSubagentAnnounceFlow: typeof runSubagentAnnounceFlow = async (params) => { + const defaultRunSubagentAnnounceFlow: RunSubagentAnnounceFlow = async (params) => { const statusLabel = params.outcome?.status === "timeout" ? "timed out" : "completed successfully"; const requesterSessionKey = resolveRequesterStoreKey( @@ -79,6 +73,8 @@ const hoisted = vi.hoisted(() => { return true; }; + const defaultCaptureSubagentCompletionReply: CaptureSubagentCompletionReply = async () => + undefined; const state = { get configOverride() { return configOverride; @@ -87,6 +83,8 @@ const hoisted = vi.hoisted(() => { configOverride = next; }, hookRunnerOverride: null as SessionsSpawnHookRunner, + defaultCaptureSubagentCompletionReply, + captureSubagentCompletionReplyOverride: defaultCaptureSubagentCompletionReply, defaultRunSubagentAnnounceFlow, runSubagentAnnounceFlowOverride: defaultRunSubagentAnnounceFlow, }; @@ -131,7 +129,7 @@ export function setSessionsSpawnHookRunnerOverride(next: SessionsSpawnHookRunner hoisted.state.hookRunnerOverride = next; } -export function setSessionsSpawnAnnounceFlowOverride(next: typeof runSubagentAnnounceFlow): void { +export function setSessionsSpawnAnnounceFlowOverride(next: RunSubagentAnnounceFlow): void { hoisted.state.runSubagentAnnounceFlowOverride = next; } @@ -142,22 +140,11 @@ export async function getSessionsSpawnTool(opts: CreateOpenClawToolsOpts) { 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, + captureSubagentCompletionReply: (sessionKey) => + hoisted.state.captureSubagentCompletionReplyOverride(sessionKey), runSubagentAnnounceFlow: (params) => hoisted.state.runSubagentAnnounceFlowOverride(params), }); if (!cachedCreateSessionsSpawnTool) { diff --git a/src/agents/sessions-spawn-hooks.test.ts b/src/agents/sessions-spawn-hooks.test.ts index 4330c8115cd..bb9206873b3 100644 --- a/src/agents/sessions-spawn-hooks.test.ts +++ b/src/agents/sessions-spawn-hooks.test.ts @@ -1,15 +1,32 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import "./test-helpers/fast-core-tools.js"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { - findGatewayRequest, - getCallGatewayMock, - getGatewayMethods, - getSessionsSpawnTool, - resetSessionsSpawnHookRunnerOverride, - setSessionsSpawnConfigOverride, - setSessionsSpawnHookRunnerOverride, -} from "./openclaw-tools.subagents.sessions-spawn.test-harness.js"; -import { resetSubagentRegistryForTests } from "./subagent-registry.js"; + createSubagentSpawnTestConfig, + loadSubagentSpawnModuleForTest, +} from "./subagent-spawn.test-helpers.js"; + +type GatewayRequest = { method?: string; params?: Record }; + +const hoisted = vi.hoisted(() => ({ + callGatewayMock: vi.fn(), + configOverride: { + session: { mainKey: "main", scope: "per-sender" }, + tools: { + sessions_spawn: { + attachments: { + enabled: true, + maxFiles: 50, + maxFileBytes: 1 * 1024 * 1024, + maxTotalBytes: 5 * 1024 * 1024, + }, + }, + }, + agents: { + defaults: { + workspace: "/tmp", + }, + }, + }, +})); const hookRunnerMocks = vi.hoisted(() => ({ hasSubagentEndedHook: true, @@ -38,6 +55,58 @@ const hookRunnerMocks = vi.hoisted(() => ({ runSubagentEnded: vi.fn(async () => {}), })); +let resetSubagentRegistryForTests: typeof import("./subagent-registry.js").resetSubagentRegistryForTests; +let spawnSubagentDirect: typeof import("./subagent-spawn.js").spawnSubagentDirect; + +function getGatewayRequests(): GatewayRequest[] { + return hoisted.callGatewayMock.mock.calls.map((call) => call[0] as GatewayRequest); +} + +function getGatewayMethods() { + return getGatewayRequests().map((request) => request.method); +} + +function findGatewayRequest(method: string): GatewayRequest | undefined { + return getGatewayRequests().find((request) => request.method === method); +} + +function setConfig(next: Record) { + hoisted.configOverride = createSubagentSpawnTestConfig(undefined, next); +} + +async function spawn(params?: { + toolCallId?: string; + task?: string; + label?: string; + runTimeoutSeconds?: number; + thread?: boolean; + mode?: "run" | "session"; + agentSessionKey?: string; + agentChannel?: string; + agentAccountId?: string; + agentTo?: string; + agentThreadId?: string | number; +}) { + return await spawnSubagentDirect( + { + task: params?.task ?? "do thing", + ...(params?.label ? { label: params.label } : {}), + ...(typeof params?.runTimeoutSeconds === "number" + ? { runTimeoutSeconds: params.runTimeoutSeconds } + : {}), + ...(params?.thread ? { thread: true } : {}), + ...(params?.mode ? { mode: params.mode } : {}), + }, + { + agentSessionKey: params?.agentSessionKey ?? "main", + agentChannel: params?.agentChannel ?? "discord", + agentAccountId: params?.agentAccountId, + agentTo: params?.agentTo, + agentThreadId: params?.agentThreadId, + }, + ); +} + function expectSessionsDeleteWithoutAgentStart() { const methods = getGatewayMethods(); expect(methods).toContain("sessions.delete"); @@ -45,8 +114,7 @@ function expectSessionsDeleteWithoutAgentStart() { } function mockAgentStartFailure() { - const callGatewayMock = getCallGatewayMock(); - callGatewayMock.mockImplementation(async (opts: unknown) => { + hoisted.callGatewayMock.mockImplementation(async (opts: unknown) => { const request = opts as { method?: string }; if (request.method === "agent") { throw new Error("spawn failed"); @@ -55,47 +123,6 @@ function mockAgentStartFailure() { }); } -async function runSessionThreadSpawnAndGetError(params: { - toolCallId: string; - spawningResult: { status: "error"; error: string } | { status: "ok"; threadBindingReady: false }; -}): Promise<{ error?: string; childSessionKey?: string }> { - hookRunnerMocks.runSubagentSpawning.mockResolvedValueOnce(params.spawningResult); - const tool = await getSessionsSpawnTool({ - agentSessionKey: "main", - agentChannel: "discord", - agentAccountId: "work", - agentTo: "channel:123", - }); - - const result = await tool.execute(params.toolCallId, { - task: "do thing", - runTimeoutSeconds: 1, - thread: true, - mode: "session", - }); - expect(result.details).toMatchObject({ status: "error" }); - return result.details as { error?: string; childSessionKey?: string }; -} - -async function getDiscordThreadSessionTool() { - return await getSessionsSpawnTool({ - agentSessionKey: "main", - agentChannel: "discord", - agentAccountId: "work", - agentTo: "channel:123", - agentThreadId: "456", - }); -} - -async function executeDiscordThreadSessionSpawn(toolCallId: string) { - const tool = await getDiscordThreadSessionTool(); - return await tool.execute(toolCallId, { - task: "do thing", - thread: true, - mode: "session", - }); -} - function getSpawnedEventCall(): Record { const [event] = (hookRunnerMocks.runSubagentSpawned.mock.calls[0] ?? []) as unknown as [ Record, @@ -103,35 +130,33 @@ function getSpawnedEventCall(): Record { return event; } -function expectErrorResultMessage(result: { details: unknown }, pattern: RegExp): void { - expect(result.details).toMatchObject({ status: "error" }); - const details = result.details as { error?: string }; - expect(details.error).toMatch(pattern); +function expectErrorResultMessage( + result: { error?: string; status: string }, + pattern: RegExp, +): void { + expect(result.status).toBe("error"); + expect(result.error).toMatch(pattern); } function expectThreadBindFailureCleanup( - details: { childSessionKey?: string; error?: string }, + result: { childSessionKey?: string; error?: string }, pattern: RegExp, ): void { - expect(details.error).toMatch(pattern); + expect(result.error).toMatch(pattern); expect(hookRunnerMocks.runSubagentSpawned).not.toHaveBeenCalled(); expectSessionsDeleteWithoutAgentStart(); const deleteCall = findGatewayRequest("sessions.delete"); expect(deleteCall?.params).toMatchObject({ - key: details.childSessionKey, + key: result.childSessionKey, emitLifecycleHooks: false, }); } -describe("sessions_spawn subagent lifecycle hooks", () => { - beforeEach(() => { - resetSubagentRegistryForTests(); - resetSessionsSpawnHookRunnerOverride(); - hookRunnerMocks.hasSubagentEndedHook = true; - hookRunnerMocks.runSubagentSpawning.mockClear(); - hookRunnerMocks.runSubagentSpawned.mockClear(); - hookRunnerMocks.runSubagentEnded.mockClear(); - setSessionsSpawnHookRunnerOverride({ +beforeAll(async () => { + ({ resetSubagentRegistryForTests, spawnSubagentDirect } = await loadSubagentSpawnModuleForTest({ + callGatewayMock: hoisted.callGatewayMock, + loadConfig: () => hoisted.configOverride, + hookRunner: { hasHooks: (hookName: string) => hookName === "subagent_spawning" || hookName === "subagent_spawned" || @@ -139,22 +164,36 @@ describe("sessions_spawn subagent lifecycle hooks", () => { runSubagentSpawning: hookRunnerMocks.runSubagentSpawning, runSubagentSpawned: hookRunnerMocks.runSubagentSpawned, runSubagentEnded: hookRunnerMocks.runSubagentEnded, - }); - const callGatewayMock = getCallGatewayMock(); - callGatewayMock.mockClear(); - setSessionsSpawnConfigOverride({ + }, + resetModules: false, + sessionStorePath: "/tmp/subagent-spawn-hooks-session-store.json", + })); +}); + +describe("sessions_spawn subagent lifecycle hooks", () => { + beforeEach(() => { + resetSubagentRegistryForTests(); + hoisted.callGatewayMock.mockReset(); + hookRunnerMocks.hasSubagentEndedHook = true; + hookRunnerMocks.runSubagentSpawning.mockClear(); + hookRunnerMocks.runSubagentSpawned.mockClear(); + hookRunnerMocks.runSubagentEnded.mockClear(); + setConfig({ session: { mainKey: "main", scope: "per-sender", }, }); - callGatewayMock.mockImplementation(async (opts: unknown) => { + hoisted.callGatewayMock.mockImplementation(async (opts: unknown) => { const request = opts as { method?: string }; - if (request.method === "agent") { - return { runId: "run-1", status: "accepted", acceptedAt: 1 }; + if (request.method === "sessions.patch") { + return { ok: true }; } - if (request.method === "agent.wait") { - return { runId: "run-1", status: "running" }; + if (request.method === "sessions.delete") { + return { ok: true }; + } + if (request.method === "agent") { + return { runId: "run-1", status: "accepted", acceptedAt: 1_001 }; } return {}; }); @@ -165,22 +204,16 @@ describe("sessions_spawn subagent lifecycle hooks", () => { }); it("runs subagent_spawning and emits subagent_spawned with requester metadata", async () => { - const tool = await getSessionsSpawnTool({ - agentSessionKey: "main", - agentChannel: "discord", + const result = await spawn({ + label: "research", + runTimeoutSeconds: 1, + thread: true, agentAccountId: "work", agentTo: "channel:123", agentThreadId: 456, }); - const result = await tool.execute("call", { - task: "do thing", - label: "research", - runTimeoutSeconds: 1, - thread: true, - }); - - expect(result.details).toMatchObject({ status: "accepted", runId: "run-1" }); + expect(result).toMatchObject({ status: "accepted", runId: "run-1" }); expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledTimes(1); expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledWith( { @@ -229,18 +262,12 @@ describe("sessions_spawn subagent lifecycle hooks", () => { }); it("emits subagent_spawned with threadRequested=false when not requested", async () => { - const tool = await getSessionsSpawnTool({ - agentSessionKey: "main", - agentChannel: "discord", + const result = await spawn({ + runTimeoutSeconds: 1, agentTo: "channel:123", }); - const result = await tool.execute("call2", { - task: "do thing", - runTimeoutSeconds: 1, - }); - - expect(result.details).toMatchObject({ status: "accepted", runId: "run-1" }); + expect(result).toMatchObject({ status: "accepted", runId: "run-1" }); expect(hookRunnerMocks.runSubagentSpawning).not.toHaveBeenCalled(); expect(hookRunnerMocks.runSubagentSpawned).toHaveBeenCalledTimes(1); const [event] = (hookRunnerMocks.runSubagentSpawned.mock.calls[0] ?? []) as unknown as [ @@ -257,20 +284,14 @@ describe("sessions_spawn subagent lifecycle hooks", () => { }); it("respects explicit mode=run when thread binding is requested", async () => { - const tool = await getSessionsSpawnTool({ - agentSessionKey: "main", - agentChannel: "discord", - agentTo: "channel:123", - }); - - const result = await tool.execute("call3", { - task: "do thing", + const result = await spawn({ runTimeoutSeconds: 1, thread: true, mode: "run", + agentTo: "channel:123", }); - expect(result.details).toMatchObject({ status: "accepted", runId: "run-1", mode: "run" }); + expect(result).toMatchObject({ status: "accepted", runId: "run-1", mode: "run" }); expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledTimes(1); const event = getSpawnedEventCall(); expect(event).toMatchObject({ @@ -280,57 +301,57 @@ describe("sessions_spawn subagent lifecycle hooks", () => { }); it("returns error when thread binding cannot be created", async () => { - const details = await runSessionThreadSpawnAndGetError({ + hookRunnerMocks.runSubagentSpawning.mockResolvedValueOnce({ + status: "error", + error: "Unable to create or bind a Discord thread for this subagent session.", + }); + const result = await spawn({ toolCallId: "call4", - spawningResult: { - status: "error", - error: "Unable to create or bind a Discord thread for this subagent session.", - }, - }); - expectThreadBindFailureCleanup(details, /thread/i); - }); - - it("returns error when thread binding is not marked ready", async () => { - const details = await runSessionThreadSpawnAndGetError({ - toolCallId: "call4b", - spawningResult: { - status: "ok", - threadBindingReady: false, - }, - }); - expectThreadBindFailureCleanup(details, /unable to create or bind a thread/i); - }); - - it("rejects mode=session when thread=true is not requested", async () => { - const tool = await getSessionsSpawnTool({ - agentSessionKey: "main", - agentChannel: "discord", + runTimeoutSeconds: 1, + thread: true, + mode: "session", + agentAccountId: "work", agentTo: "channel:123", }); - const result = await tool.execute("call6", { - task: "do thing", + expectThreadBindFailureCleanup(result, /thread/i); + }); + + it("returns error when thread binding is not marked ready", async () => { + hookRunnerMocks.runSubagentSpawning.mockResolvedValueOnce({ + status: "ok", + threadBindingReady: false, + }); + const result = await spawn({ + toolCallId: "call4b", + runTimeoutSeconds: 1, + thread: true, mode: "session", + agentAccountId: "work", + agentTo: "channel:123", + }); + + expectThreadBindFailureCleanup(result, /unable to create or bind a thread/i); + }); + + it("rejects mode=session when thread=true is not requested", async () => { + const result = await spawn({ + mode: "session", + agentTo: "channel:123", }); expectErrorResultMessage(result, /requires thread=true/i); expect(hookRunnerMocks.runSubagentSpawning).not.toHaveBeenCalled(); expect(hookRunnerMocks.runSubagentSpawned).not.toHaveBeenCalled(); - const callGatewayMock = getCallGatewayMock(); - expect(callGatewayMock).not.toHaveBeenCalled(); + expect(hoisted.callGatewayMock).not.toHaveBeenCalled(); }); it("rejects thread=true on channels without thread support", async () => { - const tool = await getSessionsSpawnTool({ - agentSessionKey: "main", - agentChannel: "signal", - agentTo: "+123", - }); - - const result = await tool.execute("call5", { - task: "do thing", + const result = await spawn({ thread: true, mode: "session", + agentChannel: "signal", + agentTo: "+123", }); expectErrorResultMessage(result, /only discord/i); @@ -341,9 +362,15 @@ describe("sessions_spawn subagent lifecycle hooks", () => { it("runs subagent_ended cleanup hook when agent start fails after successful bind", async () => { mockAgentStartFailure(); - const result = await executeDiscordThreadSessionSpawn("call7"); + const result = await spawn({ + thread: true, + mode: "session", + agentAccountId: "work", + agentTo: "channel:123", + agentThreadId: "456", + }); - expect(result.details).toMatchObject({ status: "error" }); + expect(result).toMatchObject({ status: "error" }); expect(hookRunnerMocks.runSubagentEnded).toHaveBeenCalledTimes(1); const [event] = (hookRunnerMocks.runSubagentEnded.mock.calls[0] ?? []) as unknown as [ Record, @@ -368,9 +395,15 @@ describe("sessions_spawn subagent lifecycle hooks", () => { it("falls back to sessions.delete cleanup when subagent_ended hook is unavailable", async () => { hookRunnerMocks.hasSubagentEndedHook = false; mockAgentStartFailure(); - const result = await executeDiscordThreadSessionSpawn("call8"); + const result = await spawn({ + thread: true, + mode: "session", + agentAccountId: "work", + agentTo: "channel:123", + agentThreadId: "456", + }); - expect(result.details).toMatchObject({ status: "error" }); + expect(result).toMatchObject({ status: "error" }); expect(hookRunnerMocks.runSubagentEnded).not.toHaveBeenCalled(); const methods = getGatewayMethods(); expect(methods).toContain("sessions.delete"); @@ -382,8 +415,7 @@ describe("sessions_spawn subagent lifecycle hooks", () => { }); it("cleans up the provisional session when lineage patching fails after thread binding", async () => { - const callGatewayMock = getCallGatewayMock(); - callGatewayMock.mockImplementation(async (opts: unknown) => { + hoisted.callGatewayMock.mockImplementation(async (opts: unknown) => { const request = opts as { method?: string; params?: Record }; if (request.method === "sessions.patch" && typeof request.params?.spawnedBy === "string") { throw new Error("lineage patch failed"); @@ -391,12 +423,21 @@ describe("sessions_spawn subagent lifecycle hooks", () => { if (request.method === "sessions.delete") { return { ok: true }; } + if (request.method === "agent") { + return { runId: "run-1", status: "accepted", acceptedAt: 1_001 }; + } return {}; }); - const result = await executeDiscordThreadSessionSpawn("call9"); + const result = await spawn({ + thread: true, + mode: "session", + agentAccountId: "work", + agentTo: "channel:123", + agentThreadId: "456", + }); - expect(result.details).toMatchObject({ + expect(result).toMatchObject({ status: "error", error: "lineage patch failed", }); @@ -407,7 +448,7 @@ describe("sessions_spawn subagent lifecycle hooks", () => { expect(methods).not.toContain("agent"); const deleteCall = findGatewayRequest("sessions.delete"); expect(deleteCall?.params).toMatchObject({ - key: (result.details as { childSessionKey?: string }).childSessionKey, + key: result.childSessionKey, deleteTranscript: true, emitLifecycleHooks: true, }); diff --git a/src/agents/subagent-announce-delivery.ts b/src/agents/subagent-announce-delivery.ts index 425309962e1..f7ad4cef642 100644 --- a/src/agents/subagent-announce-delivery.ts +++ b/src/agents/subagent-announce-delivery.ts @@ -1,5 +1,5 @@ import type { ConversationRef } from "../infra/outbound/session-binding-service.js"; -import { normalizeAccountId, normalizeMainKey } from "../routing/session-key.js"; +import { normalizeAccountId } from "../routing/session-key.js"; import { defaultRuntime } from "../runtime.js"; import { isCronSessionKey } from "../sessions/session-key-utils.js"; import { @@ -26,7 +26,6 @@ import { resolveAgentIdFromSessionKey, resolveConversationIdFromTargets, resolveExternalBestEffortDeliveryTarget, - resolveMainSessionKey, resolveQueueSettings, resolveStorePath, } from "./subagent-announce-delivery.runtime.js"; @@ -37,6 +36,7 @@ import { import { resolveAnnounceOrigin, type DeliveryContext } from "./subagent-announce-origin.js"; import { type AnnounceQueueItem, enqueueAnnounce } from "./subagent-announce-queue.js"; import { getSubagentDepthFromSessionStore } from "./subagent-depth.js"; +import { resolveRequesterStoreKey } from "./subagent-requester-store-key.js"; import type { SpawnSubagentMode } from "./subagent-spawn.js"; export { resolveAnnounceOrigin } from "./subagent-announce-origin.js"; @@ -302,28 +302,6 @@ async function sendAnnounce(item: AnnounceQueueItem) { }); } -export function resolveRequesterStoreKey( - cfg: ReturnType, - requesterSessionKey: string, -): string { - const raw = (requesterSessionKey ?? "").trim(); - if (!raw) { - return raw; - } - if (raw === "global" || raw === "unknown") { - return raw; - } - if (raw.startsWith("agent:")) { - return raw; - } - const mainKey = normalizeMainKey(cfg.session?.mainKey); - if (raw === "main" || raw === mainKey) { - return resolveMainSessionKey(cfg); - } - const agentId = resolveAgentIdFromSessionKey(raw); - return `agent:${agentId}:${raw}`; -} - export function loadRequesterSessionEntry(requesterSessionKey: string) { const cfg = subagentAnnounceDeliveryDeps.loadConfig(); const canonicalKey = resolveRequesterStoreKey(cfg, requesterSessionKey); diff --git a/src/agents/subagent-requester-store-key.ts b/src/agents/subagent-requester-store-key.ts new file mode 100644 index 00000000000..72392aac042 --- /dev/null +++ b/src/agents/subagent-requester-store-key.ts @@ -0,0 +1,32 @@ +import { + resolveAgentIdFromSessionKey, + resolveMainSessionKey, +} from "../config/sessions/main-session.js"; +import { normalizeMainKey } from "../routing/session-key.js"; + +type RequesterStoreKeyConfig = { + session?: { mainKey?: string }; + agents?: { list?: Array<{ id?: string; default?: boolean }> }; +}; + +export function resolveRequesterStoreKey( + cfg: RequesterStoreKeyConfig | undefined, + requesterSessionKey: string, +): string { + const raw = (requesterSessionKey ?? "").trim(); + if (!raw) { + return raw; + } + if (raw === "global" || raw === "unknown") { + return raw; + } + if (raw.startsWith("agent:")) { + return raw; + } + const mainKey = normalizeMainKey(cfg?.session?.mainKey); + if (raw === "main" || raw === mainKey) { + return resolveMainSessionKey(cfg); + } + const agentId = resolveAgentIdFromSessionKey(raw); + return `agent:${agentId}:${raw}`; +} diff --git a/src/agents/subagent-spawn.test-helpers.ts b/src/agents/subagent-spawn.test-helpers.ts index e434ec3f893..762ec620560 100644 --- a/src/agents/subagent-spawn.test-helpers.ts +++ b/src/agents/subagent-spawn.test-helpers.ts @@ -8,7 +8,8 @@ type MockImplementationTarget = { }; type SessionStore = Record>; type SessionStoreMutator = (store: SessionStore) => unknown; -type HookRunner = Pick; +type HookRunner = Pick & + Partial>; type SubagentSpawnModuleForTest = Awaited & { resetSubagentRegistryForTests: MockFn; };