diff --git a/src/agents/pi-embedded-runner/run/attempt-bootstrap-routing.ts b/src/agents/pi-embedded-runner/run/attempt-bootstrap-routing.ts new file mode 100644 index 00000000000..84ad597ae22 --- /dev/null +++ b/src/agents/pi-embedded-runner/run/attempt-bootstrap-routing.ts @@ -0,0 +1,71 @@ +import type { BootstrapMode } from "../../bootstrap-mode.js"; +import { resolveBootstrapMode } from "../../bootstrap-mode.js"; +import { buildAgentUserPromptPrefix } from "../../system-prompt.js"; + +export type AttemptBootstrapRoutingInput = { + workspaceBootstrapPending: boolean; + bootstrapContextRunKind?: "default" | "heartbeat" | "cron"; + trigger?: string; + sessionKey?: string; + isPrimaryRun: boolean; + isCanonicalWorkspace?: boolean; + effectiveWorkspace: string; + resolvedWorkspace: string; + hasBootstrapFileAccess: boolean; +}; + +export type AttemptBootstrapRouting = { + bootstrapMode: BootstrapMode; + shouldStripBootstrapFromContext: boolean; + userPromptPrefixText?: string; +}; + +export type AttemptWorkspaceBootstrapRoutingInput = Omit< + AttemptBootstrapRoutingInput, + "workspaceBootstrapPending" +> & { + isWorkspaceBootstrapPending: (workspaceDir: string) => Promise; +}; + +export function shouldStripBootstrapFromEmbeddedContext(_params: { + bootstrapMode: BootstrapMode; +}): boolean { + return true; +} + +export function resolveAttemptBootstrapRouting( + params: AttemptBootstrapRoutingInput, +): AttemptBootstrapRouting { + const bootstrapMode = resolveBootstrapMode({ + bootstrapPending: params.workspaceBootstrapPending, + runKind: params.bootstrapContextRunKind ?? "default", + isInteractiveUserFacing: params.trigger === "user" || params.trigger === "manual", + isPrimaryRun: params.isPrimaryRun, + isCanonicalWorkspace: + (params.isCanonicalWorkspace ?? true) && + params.effectiveWorkspace === params.resolvedWorkspace, + hasBootstrapFileAccess: params.hasBootstrapFileAccess, + }); + + return { + bootstrapMode, + shouldStripBootstrapFromContext: shouldStripBootstrapFromEmbeddedContext({ + bootstrapMode, + }), + userPromptPrefixText: buildAgentUserPromptPrefix({ + bootstrapMode, + }), + }; +} + +export async function resolveAttemptWorkspaceBootstrapRouting( + params: AttemptWorkspaceBootstrapRoutingInput, +): Promise { + const workspaceBootstrapPending = await params.isWorkspaceBootstrapPending( + params.resolvedWorkspace, + ); + return resolveAttemptBootstrapRouting({ + ...params, + workspaceBootstrapPending, + }); +} diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-routing.test.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-routing.test.ts index a0acd53906e..060cd63ea4d 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-routing.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-routing.test.ts @@ -1,60 +1,28 @@ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { - cleanupTempPaths, - createContextEngineAttemptRunner, - getHoisted, - resetEmbeddedAttemptHarness, -} from "./attempt.spawn-workspace.test-support.js"; - -const hoisted = getHoisted(); +import { describe, expect, it, vi } from "vitest"; +import { resolveAttemptWorkspaceBootstrapRouting } from "./attempt-bootstrap-routing.js"; describe("runEmbeddedAttempt bootstrap routing", () => { - const tempPaths: string[] = []; - - beforeEach(() => { - resetEmbeddedAttemptHarness(); - }); - - afterEach(async () => { - await cleanupTempPaths(tempPaths); - }); - it("resolves bootstrap pending from the canonical workspace instead of a copied sandbox", async () => { const sandboxWorkspace = "/tmp/openclaw-sandbox-copy"; - let capturedPrompt = ""; - - hoisted.resolveSandboxContextMock.mockResolvedValue({ - enabled: true, - workspaceAccess: "ro", - workspaceDir: sandboxWorkspace, - }); - hoisted.isWorkspaceBootstrapPendingMock.mockImplementation(async (workspaceDir: string) => { + const canonicalWorkspace = "/tmp/openclaw-canonical-workspace"; + const isWorkspaceBootstrapPending = vi.fn(async (workspaceDir: string) => { return workspaceDir === sandboxWorkspace; }); - await createContextEngineAttemptRunner({ - sessionKey: "agent:main:bootstrap-canonical-workspace", - tempPaths, - contextEngine: { - assemble: async ({ messages }) => ({ - messages, - estimatedTokens: 1, - }), - }, - attemptOverrides: { - disableTools: true, - }, - sessionPrompt: async (session, prompt) => { - capturedPrompt = prompt; - session.messages = [ - ...session.messages, - { role: "assistant", content: "done", timestamp: 2 } as never, - ]; - }, + const routing = await resolveAttemptWorkspaceBootstrapRouting({ + isWorkspaceBootstrapPending, + trigger: "user", + isPrimaryRun: true, + isCanonicalWorkspace: true, + effectiveWorkspace: sandboxWorkspace, + resolvedWorkspace: canonicalWorkspace, + hasBootstrapFileAccess: true, }); - expect(hoisted.isWorkspaceBootstrapPendingMock).toHaveBeenCalledTimes(1); - expect(hoisted.isWorkspaceBootstrapPendingMock).not.toHaveBeenCalledWith(sandboxWorkspace); - expect(capturedPrompt).not.toContain("[Bootstrap pending]"); + expect(isWorkspaceBootstrapPending).toHaveBeenCalledOnce(); + expect(isWorkspaceBootstrapPending).toHaveBeenCalledWith(canonicalWorkspace); + expect(isWorkspaceBootstrapPending).not.toHaveBeenCalledWith(sandboxWorkspace); + expect(routing.bootstrapMode).toBe("none"); + expect(routing.userPromptPrefixText).toBeUndefined(); }); }); diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 5b77dbf1884..df08822a05e 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -1,6 +1,6 @@ -import path from "node:path"; import fs from "node:fs/promises"; import os from "node:os"; +import path from "node:path"; import type { AgentMessage } from "@mariozechner/pi-agent-core"; import { createAgentSession, @@ -52,7 +52,6 @@ import { resolveBootstrapContextForRun, resolveContextInjectionMode, } from "../../bootstrap-files.js"; -import { resolveBootstrapMode } from "../../bootstrap-mode.js"; import { createCacheTrace } from "../../cache-trace.js"; import { listChannelSupportedActions, @@ -114,7 +113,6 @@ import { import { resolveSystemPromptOverride } from "../../system-prompt-override.js"; import { buildSystemPromptParams } from "../../system-prompt-params.js"; import { buildSystemPromptReport } from "../../system-prompt-report.js"; -import { buildAgentUserPromptPrefix } from "../../system-prompt.js"; import { resolveAgentTimeoutMs } from "../../timeout.js"; import { UNKNOWN_TOOL_THRESHOLD } from "../../tool-loop-detection.js"; import { @@ -182,6 +180,11 @@ import { splitSdkTools } from "../tool-split.js"; import { mapThinkingLevel } from "../utils.js"; import { flushPendingToolResultsAfterIdle } from "../wait-for-idle-before-flush.js"; export { buildContextEnginePromptCacheInfo } from "./attempt.context-engine-helpers.js"; +import { + resolveAttemptWorkspaceBootstrapRouting, + shouldStripBootstrapFromEmbeddedContext, +} from "./attempt-bootstrap-routing.js"; +export { shouldStripBootstrapFromEmbeddedContext } from "./attempt-bootstrap-routing.js"; import { configureEmbeddedAttemptHttpRuntime } from "./attempt-http-runtime.js"; import { assembleAttemptContextEngine, @@ -318,12 +321,6 @@ export function resolveUnknownToolGuardThreshold(loopDetection?: { return UNKNOWN_TOOL_THRESHOLD; } -export function shouldStripBootstrapFromEmbeddedContext(_params: { - bootstrapMode: "full" | "limited" | "none"; -}): boolean { - return true; -} - export function isPrimaryBootstrapRun(sessionKey?: string): boolean { return !isSubagentSessionKey(sessionKey) && !isAcpSessionKey(sessionKey); } @@ -338,8 +335,7 @@ export function remapInjectedContextFilesToWorkspace(params: { } return params.files.map((file) => { const relative = path.relative(params.sourceWorkspaceDir, file.path); - const canRemap = - relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); + const canRemap = relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); return canRemap ? { ...file, @@ -484,8 +480,6 @@ export async function runEmbeddedAttempt( const sessionLabel = params.sessionKey ?? params.sessionId; const contextInjectionMode = resolveContextInjectionMode(params.config); - // Bootstrap lifecycle is owned by the canonical workspace, not a copied sandbox view. - const workspaceBootstrapPending = await isWorkspaceBootstrapPending(resolvedWorkspace); const agentDir = params.agentDir ?? resolveOpenClawAgentDir(); const toolsRaw = params.disableTools ? [] @@ -555,20 +549,20 @@ export async function runEmbeddedAttempt( return allTools; })(); const toolsEnabled = supportsModelTools(params.model); - const bootstrapRunKind = params.bootstrapContextRunKind ?? "default"; const bootstrapHasFileAccess = toolsEnabled && toolsRaw.some((tool) => tool.name === "read"); - const bootstrapMode = resolveBootstrapMode({ - bootstrapPending: workspaceBootstrapPending, - runKind: bootstrapRunKind, - isInteractiveUserFacing: params.trigger === "user" || params.trigger === "manual", + const bootstrapRouting = await resolveAttemptWorkspaceBootstrapRouting({ + isWorkspaceBootstrapPending, + bootstrapContextRunKind: params.bootstrapContextRunKind, + trigger: params.trigger, + sessionKey: params.sessionKey, isPrimaryRun: isPrimaryBootstrapRun(params.sessionKey), - isCanonicalWorkspace: - (params.isCanonicalWorkspace ?? true) && effectiveWorkspace === resolvedWorkspace, + isCanonicalWorkspace: params.isCanonicalWorkspace, + effectiveWorkspace, + resolvedWorkspace, hasBootstrapFileAccess: bootstrapHasFileAccess, }); - const shouldStripBootstrapFromContext = shouldStripBootstrapFromEmbeddedContext({ - bootstrapMode, - }); + const bootstrapMode = bootstrapRouting.bootstrapMode; + const shouldStripBootstrapFromContext = bootstrapRouting.shouldStripBootstrapFromContext; const { bootstrapFiles: hookAdjustedBootstrapFiles, contextFiles: resolvedContextFiles, @@ -576,7 +570,7 @@ export async function runEmbeddedAttempt( } = await resolveAttemptBootstrapContext({ contextInjectionMode, bootstrapContextMode: params.bootstrapContextMode, - bootstrapContextRunKind: bootstrapRunKind, + bootstrapContextRunKind: params.bootstrapContextRunKind ?? "default", bootstrapMode, sessionFile: params.sessionFile, hasCompletedBootstrapTurn, @@ -927,9 +921,7 @@ export async function runEmbeddedAttempt( }); const systemPromptOverride = createSystemPromptOverride(appendPrompt); let systemPromptText = systemPromptOverride(); - const userPromptPrefixText = buildAgentUserPromptPrefix({ - bootstrapMode, - }); + const userPromptPrefixText = bootstrapRouting.userPromptPrefixText; let sessionManager: ReturnType | undefined; let session: Awaited>["session"] | undefined;