diff --git a/CHANGELOG.md b/CHANGELOG.md index 4efe38cfc66..c192d95ceb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/agents/command/attempt-execution.ts b/src/agents/command/attempt-execution.ts index b68ba0cd165..0614f0fcead 100644 --- a/src/agents/command/attempt-execution.ts +++ b/src/agents/command/attempt-execution.ts @@ -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, diff --git a/src/agents/command/types.ts b/src/agents/command/types.ts index 3a2a6c0184a..b01b7af1b8c 100644 --- a/src/agents/command/types.ts +++ b/src/agents/command/types.ts @@ -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). */ diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 5588aa91213..56df07f50fa 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -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], diff --git a/src/agents/subagent-spawn.ts b/src/agents/subagent-spawn.ts index 47aa689aad6..6c98eba189e 100644 --- a/src/agents/subagent-spawn.ts +++ b/src/agents/subagent-spawn.ts @@ -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, diff --git a/src/agents/subagent-spawn.workspace.test.ts b/src/agents/subagent-spawn.workspace.test.ts index 470c1bebbcd..3341b4cb43e 100644 --- a/src/agents/subagent-spawn.workspace.test.ts +++ b/src/agents/subagent-spawn.workspace.test.ts @@ -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 } | 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: { diff --git a/src/agents/tools/sessions-spawn-tool.test.ts b/src/agents/tools/sessions-spawn-tool.test.ts index 88892c92f12..0722e5d8c63 100644 --- a/src/agents/tools/sessions-spawn-tool.test.ts +++ b/src/agents/tools/sessions-spawn-tool.test.ts @@ -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", diff --git a/src/agents/tools/sessions-spawn-tool.ts b/src/agents/tools/sessions-spawn-tool.ts index 4be2820b580..acfde709a35 100644 --- a/src/agents/tools/sessions-spawn-tool.ts +++ b/src/agents/tools/sessions-spawn-tool.ts @@ -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: diff --git a/src/gateway/protocol/schema/agent.ts b/src/gateway/protocol/schema/agent.ts index b2d984f0b57..3a39e4979cd 100644 --- a/src/gateway/protocol/schema/agent.ts +++ b/src/gateway/protocol/schema/agent.ts @@ -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, diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index 4fb99fbb878..1c67f17210b 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -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.