fix(plugins): late-binding subagent runtime for non-gateway load paths (#46648)

Merged via squash.

Prepared head SHA: 44742652c9
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
Josh Lehman
2026-03-16 14:27:54 -07:00
committed by GitHub
parent abce640772
commit eeb140b4f0
42 changed files with 555 additions and 28 deletions

View File

@@ -323,6 +323,7 @@ export async function runAgentTurnWithFallback(params: {
try {
const result = await runEmbeddedPiAgent({
...embeddedContext,
allowGatewaySubagentBinding: true,
trigger: params.isHeartbeat ? "heartbeat" : "user",
groupId: resolveGroupSessionKey(params.sessionCtx)?.id,
groupChannel:

View File

@@ -494,6 +494,7 @@ export async function runMemoryFlushIfNeeded(params: {
...embeddedContext,
...senderContext,
...runBaseParams,
allowGatewaySubagentBinding: true,
trigger: "memory",
memoryFlushWritePath,
prompt: resolveMemoryFlushPromptForRun({

View File

@@ -78,6 +78,7 @@ export const handleCompactCommand: CommandHandler = async (params) => {
const result = await compactEmbeddedPiSession({
sessionId,
sessionKey: params.sessionKey,
allowGatewaySubagentBinding: true,
messageChannel: params.command.channel,
groupId: params.sessionEntry.groupId,
groupChannel: params.sessionEntry.groupChannel,

View File

@@ -0,0 +1,127 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { HandleCommandsParams } from "./commands-types.js";
const { createOpenClawCodingToolsMock } = vi.hoisted(() => ({
createOpenClawCodingToolsMock: vi.fn(() => []),
}));
vi.mock("../../agents/bootstrap-files.js", () => ({
resolveBootstrapContextForRun: vi.fn(async () => ({
bootstrapFiles: [],
contextFiles: [],
})),
}));
vi.mock("../../agents/pi-tools.js", () => ({
createOpenClawCodingTools: createOpenClawCodingToolsMock,
}));
vi.mock("../../agents/sandbox.js", () => ({
resolveSandboxRuntimeStatus: vi.fn(() => ({ sandboxed: false, mode: "off" })),
}));
vi.mock("../../agents/skills.js", () => ({
buildWorkspaceSkillSnapshot: vi.fn(() => ({ prompt: "", skills: [], resolvedSkills: [] })),
}));
vi.mock("../../agents/skills/refresh.js", () => ({
getSkillsSnapshotVersion: vi.fn(() => "test-snapshot"),
}));
vi.mock("../../agents/agent-scope.js", () => ({
resolveSessionAgentIds: vi.fn(() => ({ sessionAgentId: "main" })),
}));
vi.mock("../../agents/model-selection.js", () => ({
resolveDefaultModelForAgent: vi.fn(() => ({ provider: "openai", model: "gpt-5" })),
}));
vi.mock("../../agents/system-prompt-params.js", () => ({
buildSystemPromptParams: vi.fn(() => ({
runtimeInfo: { host: "unknown", os: "unknown", arch: "unknown", node: process.version },
userTimezone: "UTC",
userTime: "12:00 PM",
userTimeFormat: "12h",
})),
}));
vi.mock("../../agents/system-prompt.js", () => ({
buildAgentSystemPrompt: vi.fn(() => "system prompt"),
}));
vi.mock("../../agents/tool-summaries.js", () => ({
buildToolSummaryMap: vi.fn(() => ({})),
}));
vi.mock("../../infra/skills-remote.js", () => ({
getRemoteSkillEligibility: vi.fn(() => false),
}));
vi.mock("../../tts/tts.js", () => ({
buildTtsSystemPromptHint: vi.fn(() => undefined),
}));
import { resolveCommandsSystemPromptBundle } from "./commands-system-prompt.js";
function makeParams(): HandleCommandsParams {
return {
ctx: {
SessionKey: "agent:main:default",
},
cfg: {},
command: {
surface: "telegram",
channel: "telegram",
ownerList: [],
senderIsOwner: true,
isAuthorizedSender: true,
rawBodyNormalized: "/context",
commandBodyNormalized: "/context",
},
directives: {},
elevated: {
enabled: true,
allowed: true,
failures: [],
},
agentId: "main",
sessionEntry: {
sessionId: "session-1",
groupId: "group-1",
groupChannel: "#general",
space: "guild-1",
spawnedBy: "agent:parent",
},
sessionKey: "agent:main:default",
workspaceDir: "/tmp/workspace",
defaultGroupActivation: () => "mention",
resolvedVerboseLevel: "off",
resolvedReasoningLevel: "off",
resolvedElevatedLevel: "off",
resolveDefaultThinkingLevel: async () => undefined,
provider: "openai",
model: "gpt-5.4",
contextTokens: 0,
isGroup: false,
} as unknown as HandleCommandsParams;
}
describe("resolveCommandsSystemPromptBundle", () => {
beforeEach(() => {
createOpenClawCodingToolsMock.mockClear();
createOpenClawCodingToolsMock.mockReturnValue([]);
});
it("opts command tool builds into gateway subagent binding", async () => {
await resolveCommandsSystemPromptBundle(makeParams());
expect(createOpenClawCodingToolsMock).toHaveBeenCalledWith(
expect.objectContaining({
allowGatewaySubagentBinding: true,
sessionKey: "agent:main:default",
workspaceDir: "/tmp/workspace",
messageProvider: "telegram",
}),
);
});
});

View File

@@ -57,6 +57,7 @@ export async function resolveCommandsSystemPromptBundle(
agentId: params.agentId,
workspaceDir,
sessionKey: params.sessionKey,
allowGatewaySubagentBinding: true,
messageProvider: params.command.channel,
groupId: params.sessionEntry?.groupId ?? undefined,
groupChannel: params.sessionEntry?.groupChannel ?? undefined,

View File

@@ -599,6 +599,7 @@ describe("/compact command", () => {
expect.objectContaining({
sessionId: "session-1",
sessionKey: "agent:main:main",
allowGatewaySubagentBinding: true,
trigger: "manual",
customInstructions: "focus on decisions",
messageChannel: "whatsapp",

View File

@@ -287,10 +287,12 @@ describe("createFollowupRunner bootstrap warning dedupe", () => {
const call = runEmbeddedPiAgentMock.mock.calls.at(-1)?.[0] as
| {
allowGatewaySubagentBinding?: boolean;
bootstrapPromptWarningSignaturesSeen?: string[];
bootstrapPromptWarningSignature?: string;
}
| undefined;
expect(call?.allowGatewaySubagentBinding).toBe(true);
expect(call?.bootstrapPromptWarningSignaturesSeen).toEqual(["sig-a", "sig-b"]);
expect(call?.bootstrapPromptWarningSignature).toBe("sig-b");
});

View File

@@ -171,6 +171,7 @@ export function createFollowupRunner(params: {
let attemptCompactionCount = 0;
try {
const result = await runEmbeddedPiAgent({
allowGatewaySubagentBinding: true,
sessionId: queued.run.sessionId,
sessionKey: queued.run.sessionKey,
agentId: queued.run.agentId,

View File

@@ -220,6 +220,7 @@ export async function handleInlineActions(params: {
agentDir,
workspaceDir,
config: cfg,
allowGatewaySubagentBinding: true,
});
const authorizedTools = applyOwnerOnlyToolPolicy(tools, command.senderIsOwner);