mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
fix: honor lightContext in spawned subagents (#62264) (thanks @theSamPadilla)
* Add lightContext support for spawned subagents * Clarify and guard lightContext usage in sessions_spawn * test: guard sessions_spawn lightContext acp misuse * fix: honor lightContext in spawned subagents (#62264) (thanks @theSamPadilla) --------- Co-authored-by: Jaz <jaz@bycrux.com> Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
@@ -54,6 +54,7 @@ Docs: https://docs.openclaw.ai
|
||||
- UI/light mode: target both root and nested WebKit scrollbar thumbs in the light theme so page-level and container scrollbars stay visible on light backgrounds. (#61753) Thanks @chziyue.
|
||||
- Matrix/onboarding: add an invite auto-join setup step with explicit off warnings and strict stable-target validation so new Matrix accounts stop silently ignoring invited rooms and fresh DM-style invites unless operators opt in. (#62168) Thanks @gumadeiras.
|
||||
- Telegram/doctor: keep top-level access-control fallback in place during multi-account normalization while still promoting legacy default auth into `accounts.default`, so existing named bots keep inherited allowlists without dropping the legacy default bot. (#62263) Thanks @obviyus.
|
||||
- Agents/subagents: honor `sessions_spawn(lightContext: true)` for spawned subagent runs by preserving lightweight bootstrap context through the gateway and embedded runner instead of silently falling back to full workspace bootstrap injection. (#62264) Thanks @theSamPadilla.
|
||||
|
||||
## 2026.4.5
|
||||
|
||||
|
||||
@@ -473,6 +473,8 @@ export function runAgentAttempt(params: {
|
||||
lane: params.opts.lane,
|
||||
abortSignal: params.opts.abortSignal,
|
||||
extraSystemPrompt: params.opts.extraSystemPrompt,
|
||||
bootstrapContextMode: params.opts.bootstrapContextMode,
|
||||
bootstrapContextRunKind: params.opts.bootstrapContextRunKind,
|
||||
internalEvents: params.opts.internalEvents,
|
||||
inputProvenance: params.opts.inputProvenance,
|
||||
streamParams: params.opts.streamParams,
|
||||
|
||||
@@ -85,6 +85,10 @@ export type AgentCommandOpts = {
|
||||
lane?: string;
|
||||
runId?: string;
|
||||
extraSystemPrompt?: string;
|
||||
/** Bootstrap workspace context injection mode for this run. */
|
||||
bootstrapContextMode?: "full" | "lightweight";
|
||||
/** Run kind hint for bootstrap context behavior. */
|
||||
bootstrapContextRunKind?: "default" | "heartbeat" | "cron";
|
||||
internalEvents?: AgentInternalEvent[];
|
||||
inputProvenance?: InputProvenance;
|
||||
/** Per-call stream param overrides (best-effort). */
|
||||
|
||||
@@ -666,6 +666,8 @@ export async function runEmbeddedPiAgent(
|
||||
ownerNumbers: params.ownerNumbers,
|
||||
enforceFinalTag: params.enforceFinalTag,
|
||||
silentExpected: params.silentExpected,
|
||||
bootstrapContextMode: params.bootstrapContextMode,
|
||||
bootstrapContextRunKind: params.bootstrapContextRunKind,
|
||||
bootstrapPromptWarningSignaturesSeen,
|
||||
bootstrapPromptWarningSignature:
|
||||
bootstrapPromptWarningSignaturesSeen[bootstrapPromptWarningSignaturesSeen.length - 1],
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
updateSessionStore,
|
||||
isAdminOnlyMethod,
|
||||
} from "./subagent-spawn.runtime.js";
|
||||
import type { BootstrapContextMode } from "./bootstrap-files.js";
|
||||
import { readStringParam } from "./tools/common.js";
|
||||
|
||||
export const SUBAGENT_SPAWN_MODES = ["run", "session"] as const;
|
||||
@@ -80,6 +81,7 @@ export type SpawnSubagentParams = {
|
||||
mode?: SpawnSubagentMode;
|
||||
cleanup?: "delete" | "keep";
|
||||
sandbox?: SpawnSubagentSandboxMode;
|
||||
lightContext?: boolean;
|
||||
expectsCompletionMessage?: boolean;
|
||||
attachments?: Array<{
|
||||
name: string;
|
||||
@@ -672,6 +674,10 @@ export async function spawnSubagentDirect(
|
||||
childSystemPrompt = `${childSystemPrompt}\n\n${materializedAttachments.systemPromptSuffix}`;
|
||||
}
|
||||
|
||||
const bootstrapContextMode: BootstrapContextMode | undefined = params.lightContext
|
||||
? "lightweight"
|
||||
: undefined;
|
||||
|
||||
const childTaskMessage = [
|
||||
`[Subagent Context] You are running as a subagent (depth ${childDepth}/${maxSpawnDepth}). Results auto-announce to your requester; do not busy-poll for status.`,
|
||||
spawnMode === "session"
|
||||
@@ -742,6 +748,8 @@ export async function spawnSubagentDirect(
|
||||
thinking: thinkingOverride,
|
||||
timeout: runTimeoutSeconds,
|
||||
label: label || undefined,
|
||||
bootstrapContextMode,
|
||||
bootstrapContextRunKind: "default",
|
||||
...publicSpawnedMetadata,
|
||||
},
|
||||
timeoutMs: 10_000,
|
||||
|
||||
@@ -148,6 +148,31 @@ describe("spawnSubagentDirect workspace inheritance", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("passes lightweight bootstrap context flags for lightContext subagent spawns", async () => {
|
||||
await spawnSubagentDirect(
|
||||
{
|
||||
task: "inspect workspace",
|
||||
lightContext: true,
|
||||
},
|
||||
{
|
||||
agentSessionKey: "agent:main:main",
|
||||
agentChannel: "telegram",
|
||||
agentAccountId: "123",
|
||||
agentTo: "456",
|
||||
workspaceDir: "/tmp/requester-workspace",
|
||||
},
|
||||
);
|
||||
|
||||
const agentCall = hoisted.callGatewayMock.mock.calls.find(
|
||||
([request]) => (request as { method?: string }).method === "agent",
|
||||
)?.[0] as { params?: Record<string, unknown> } | undefined;
|
||||
|
||||
expect(agentCall?.params).toMatchObject({
|
||||
bootstrapContextMode: "lightweight",
|
||||
bootstrapContextRunKind: "default",
|
||||
});
|
||||
});
|
||||
|
||||
it("deletes the provisional child session when a non-thread subagent start fails", async () => {
|
||||
hoisted.callGatewayMock.mockImplementation(
|
||||
async (request: {
|
||||
|
||||
@@ -103,6 +103,42 @@ describe("sessions_spawn tool", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("passes lightContext through to subagent spawns", async () => {
|
||||
const tool = createSessionsSpawnTool({
|
||||
agentSessionKey: "agent:main:main",
|
||||
});
|
||||
|
||||
await tool.execute("call-light", {
|
||||
task: "summarize this",
|
||||
lightContext: true,
|
||||
});
|
||||
|
||||
expect(hoisted.spawnSubagentDirectMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
task: "summarize this",
|
||||
lightContext: true,
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects lightContext when runtime is not "subagent"', async () => {
|
||||
const tool = createSessionsSpawnTool({
|
||||
agentSessionKey: "agent:main:main",
|
||||
});
|
||||
|
||||
await expect(
|
||||
tool.execute("call-light-acp", {
|
||||
runtime: "acp",
|
||||
task: "summarize this",
|
||||
lightContext: true,
|
||||
}),
|
||||
).rejects.toThrow("lightContext is only supported for runtime='subagent'.");
|
||||
|
||||
expect(hoisted.spawnSubagentDirectMock).not.toHaveBeenCalled();
|
||||
expect(hoisted.spawnAcpDirectMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes to ACP runtime when runtime=acp", async () => {
|
||||
const tool = createSessionsSpawnTool({
|
||||
agentSessionKey: "agent:main:main",
|
||||
|
||||
@@ -97,6 +97,12 @@ const SessionsSpawnToolSchema = Type.Object({
|
||||
cleanup: optionalStringEnum(["delete", "keep"] as const),
|
||||
sandbox: optionalStringEnum(SESSIONS_SPAWN_SANDBOX_MODES),
|
||||
streamTo: optionalStringEnum(SESSIONS_SPAWN_ACP_STREAM_TARGETS),
|
||||
lightContext: Type.Optional(
|
||||
Type.Boolean({
|
||||
description:
|
||||
"When true, spawned subagent runs use lightweight bootstrap context. Only applies to runtime='subagent'.",
|
||||
}),
|
||||
),
|
||||
|
||||
// Inline attachments (snapshot-by-value).
|
||||
// NOTE: Attachment contents are redacted from transcript persistence by sanitizeToolCallInputs.
|
||||
@@ -161,6 +167,10 @@ export function createSessionsSpawnTool(
|
||||
params.cleanup === "keep" || params.cleanup === "delete" ? params.cleanup : "keep";
|
||||
const sandbox = params.sandbox === "require" ? "require" : "inherit";
|
||||
const streamTo = params.streamTo === "parent" ? "parent" : undefined;
|
||||
const lightContext = params.lightContext === true;
|
||||
if (runtime === "acp" && lightContext) {
|
||||
throw new Error("lightContext is only supported for runtime='subagent'.");
|
||||
}
|
||||
// Back-compat: older callers used timeoutSeconds for this tool.
|
||||
const timeoutSecondsCandidate =
|
||||
typeof params.runTimeoutSeconds === "number"
|
||||
@@ -300,6 +310,7 @@ export function createSessionsSpawnTool(
|
||||
mode,
|
||||
cleanup,
|
||||
sandbox,
|
||||
lightContext,
|
||||
expectsCompletionMessage: true,
|
||||
attachments,
|
||||
attachMountPath:
|
||||
|
||||
@@ -102,6 +102,16 @@ export const AgentParamsSchema = Type.Object(
|
||||
bestEffortDeliver: Type.Optional(Type.Boolean()),
|
||||
lane: Type.Optional(Type.String()),
|
||||
extraSystemPrompt: Type.Optional(Type.String()),
|
||||
bootstrapContextMode: Type.Optional(
|
||||
Type.Union([Type.Literal("full"), Type.Literal("lightweight")]),
|
||||
),
|
||||
bootstrapContextRunKind: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Literal("default"),
|
||||
Type.Literal("heartbeat"),
|
||||
Type.Literal("cron"),
|
||||
]),
|
||||
),
|
||||
internalEvents: Type.Optional(Type.Array(AgentInternalEventSchema)),
|
||||
inputProvenance: Type.Optional(InputProvenanceSchema),
|
||||
idempotencyKey: NonEmptyString,
|
||||
|
||||
@@ -307,6 +307,8 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
groupSpace?: string;
|
||||
lane?: string;
|
||||
extraSystemPrompt?: string;
|
||||
bootstrapContextMode?: "full" | "lightweight";
|
||||
bootstrapContextRunKind?: "default" | "heartbeat" | "cron";
|
||||
internalEvents?: AgentInternalEvent[];
|
||||
idempotencyKey: string;
|
||||
timeout?: number;
|
||||
@@ -828,6 +830,8 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
runId,
|
||||
lane: request.lane,
|
||||
extraSystemPrompt: request.extraSystemPrompt,
|
||||
bootstrapContextMode: request.bootstrapContextMode,
|
||||
bootstrapContextRunKind: request.bootstrapContextRunKind,
|
||||
internalEvents: request.internalEvents,
|
||||
inputProvenance,
|
||||
// Internal-only: allow workspace override for spawned subagent runs.
|
||||
|
||||
Reference in New Issue
Block a user