diff --git a/agents/langchain-deepagents-code/Dockerfile b/agents/langchain-deepagents-code/Dockerfile index 3b2e62774a5..e3c77a03805 100644 --- a/agents/langchain-deepagents-code/Dockerfile +++ b/agents/langchain-deepagents-code/Dockerfile @@ -42,7 +42,7 @@ RUN chmod 444 /opt/nemoclaw-deepagents-code/generate-config.ts /opt/nemoclaw-dee && install -m 0755 /usr/local/lib/nemoclaw/dcode-launcher.sh /usr/local/bin/dcode.real \ && install -m 0755 /usr/local/lib/nemoclaw/dcode-launcher.sh /usr/local/bin/deepagents-code -ARG NEMOCLAW_MODEL=nvidia/nemotron-3-super-120b-a12b +ARG NEMOCLAW_MODEL=nvidia/nemotron-3-ultra-550b-a55b ARG NEMOCLAW_PROVIDER_KEY=inference ARG NEMOCLAW_UPSTREAM_PROVIDER=nvidia ARG NEMOCLAW_INFERENCE_BASE_URL=https://inference.local/v1 diff --git a/agents/langchain-deepagents-code/manifest.yaml b/agents/langchain-deepagents-code/manifest.yaml index 1d9038178a0..2b244b0ced7 100644 --- a/agents/langchain-deepagents-code/manifest.yaml +++ b/agents/langchain-deepagents-code/manifest.yaml @@ -72,6 +72,7 @@ device_pairing: false # inference.local endpoint using Deep Agents Code's OpenAI-compatible provider. inference: provider_type: openai_compatible + default_model: nvidia/nemotron-3-ultra-550b-a55b base_url_config_key: "models.providers.openai.base_url" model_config_key: "models.default" proxy_support: implicit diff --git a/agents/langchain-deepagents-code/patch-managed-deepagents-code.py b/agents/langchain-deepagents-code/patch-managed-deepagents-code.py index 8683abc8942..75266fc457c 100644 --- a/agents/langchain-deepagents-code/patch-managed-deepagents-code.py +++ b/agents/langchain-deepagents-code/patch-managed-deepagents-code.py @@ -4,8 +4,9 @@ # Source-of-truth review for this pinned third-party patch boundary: # invalidState: upstream entrypoints can independently enable credential stores, -# ambient MCP discovery, update/install flows, or child-process config paths that -# bypass NemoClaw's managed inference, policy, and integrity-bound MCP boundaries. +# ambient MCP discovery, update/install flows, first-run model selection, or +# child-process config paths that bypass NemoClaw's managed inference, policy, +# and integrity-bound MCP boundaries. # sourceBoundary: deepagents-code owns those Python entrypoints; NemoClaw owns the # sandbox image posture and therefore validates every patched symbol before build. # whyNotSourceFix: upstream 0.1.30 has no single managed-runtime hook that can @@ -861,6 +862,15 @@ def _nemoclaw_select_with_auth_check(self, model_spec: str, provider: str) -> No ModelSelectorScreen._select_with_auth_check = _nemoclaw_select_with_auth_check ''' +ONBOARDING_PATCH = r''' + +# NemoClaw-managed Deep Agents Code hardening v2. +def should_run_onboarding(state_dir=None) -> bool: + """Skip upstream first-run setup because NemoClaw owns model configuration.""" + del state_dir + return False +''' + def _top_level_functions(tree: ast.Module) -> set[str]: return { @@ -953,6 +963,7 @@ def main() -> None: "auth_ui": root / "widgets" / "auth.py", "codex_ui": root / "widgets" / "codex_auth.py", "model_selector": root / "widgets" / "model_selector.py", + "onboarding": root / "onboarding.py", "approval": root / "widgets" / "approval.py", "server": root / "server.py", "server_config": root / "_server_config.py", @@ -1090,6 +1101,9 @@ def main() -> None: "ModelSelectorScreen", {"_select_with_auth_check"}, ) + _require_functions( + paths["onboarding"], texts["onboarding"], {"should_run_onboarding"} + ) _require_methods( paths["approval"], texts["approval"], @@ -1164,6 +1178,9 @@ def main() -> None: transformed["model_selector"] = _append_patch( paths["model_selector"], texts["model_selector"], MODEL_SELECTOR_PATCH ) + transformed["onboarding"] = _append_patch( + paths["onboarding"], texts["onboarding"], ONBOARDING_PATCH + ) transformed["approval"] = _append_patch( paths["approval"], texts["approval"], APPROVAL_PATCH ) diff --git a/docs/get-started/quickstart-langchain-deepagents-code.mdx b/docs/get-started/quickstart-langchain-deepagents-code.mdx index 92ef766302a..7d4607269e1 100644 --- a/docs/get-started/quickstart-langchain-deepagents-code.mdx +++ b/docs/get-started/quickstart-langchain-deepagents-code.mdx @@ -36,9 +36,12 @@ After the terminal smoke checks, onboarding runs `dcode --version` and compares Fresh and resumed onboarding exit nonzero instead of reporting the runtime ready when the installed version is too old, uses an incompatible version scheme, or cannot be verified. If the version check fails, review the reported version error and run `nemo-deepagents rebuild` before resuming onboarding. NemoClaw writes `/sandbox/.deepagents/config.toml` with an OpenAI-compatible provider pointed at `https://inference.local/v1`, uses a scoped placeholder API key for that managed route, and sets `use_responses_api = false` for Chat Completions compatibility. +When you use NVIDIA Endpoints without selecting another model, new Deep Agents Code sandboxes default to `nvidia/nemotron-3-ultra-550b-a55b`. +This agent-specific default does not change the shared Nemotron 3 Super default for OpenClaw and Hermes. NemoClaw/OpenShell keeps real provider credentials in credential handling and does not write them into the Deep Agents config file. Deep Agents Code reaches `inference.local` through the managed OpenShell L7 proxy rather than direct sandbox DNS. The image launcher normalizes the runtime proxy environment for interactive, login-shell, and direct-exec paths and removes inherited proxy credentials and bypass entries before `dcode` starts. +Managed interactive sessions skip Deep Agents Code's upstream first-run onboarding and model picker, then open the TUI with the model selected during NemoClaw onboarding. ## Choose the Default Sandbox @@ -193,6 +196,7 @@ nemo-deepagents snapshot create --name before-change ``` If you upgrade from a release that persisted LangSmith environment values, rebuild each existing Deep Agents Code sandbox so its image includes the corrected `start.sh`. +If an existing sandbox displays `Choose a Recommended Model`, rebuild it so its image includes the managed startup behavior. `status` reports the selected harness as a terminal runtime and prints the interactive/headless command shape. If `status` reports `Runtime health: degraded` with an OOM kill count, rebuild the sandbox to restore the terminal runtime. diff --git a/docs/inference/inference-options.mdx b/docs/inference/inference-options.mdx index 4061a73b2e9..23828b53ffc 100644 --- a/docs/inference/inference-options.mdx +++ b/docs/inference/inference-options.mdx @@ -64,7 +64,9 @@ The managed install/start vLLM entry appears by default on DGX Spark and DGX Sta During interactive NVIDIA Endpoints onboarding, NemoClaw loads NVIDIA's public featured model catalog once per onboarding session and reports progress before displaying the model picker. It excludes retired or unsafe choices and corrects known catalog lag before displaying the result. If the catalog is unavailable, malformed, or contains no safe model IDs, the wizard warns you and uses the bundled fallback list shown below. -Nemotron 3 Super remains the default when it is present; otherwise, the first live featured model becomes the interactive default. +Nemotron 3 Super remains the shared default for OpenClaw and Hermes when it is present. +LangChain Deep Agents Code uses Nemotron 3 Ultra as its NVIDIA Endpoints default. +If an agent's default is unavailable, the first live featured model becomes the interactive default. If you set `NEMOCLAW_MODEL` to a safe custom model ID that is absent from the live catalog, it does not replace the live menu default. Choose **Other** to use that value as the pre-filled manual entry; NemoClaw validates it against NVIDIA Endpoints before continuing. NemoClaw neither displays nor accepts an unsafe `NEMOCLAW_MODEL` value as the manual-entry prefill. diff --git a/src/lib/agent/definition-types.ts b/src/lib/agent/definition-types.ts index 6d80ec25430..e99a92d0e49 100644 --- a/src/lib/agent/definition-types.ts +++ b/src/lib/agent/definition-types.ts @@ -43,6 +43,7 @@ export interface AgentDashboard { export interface AgentInference { provider_type?: string; provider_options?: string[]; + default_model?: string; } export type AgentMcpSupport = "bridge" | "disabled"; diff --git a/src/lib/agent/defs.test.ts b/src/lib/agent/defs.test.ts index 5066d9ab069..75d273ef76d 100644 --- a/src/lib/agent/defs.test.ts +++ b/src/lib/agent/defs.test.ts @@ -125,6 +125,7 @@ describe("agent definitions", () => { format: "toml", }); expect(deepAgentsCode.inference?.provider_type).toBe("openai_compatible"); + expect(deepAgentsCode.inference?.default_model).toBe("nvidia/nemotron-3-ultra-550b-a55b"); expect(deepAgentsCode.mcpCapability).toEqual({ support: "bridge", adapter: "deepagents-config", @@ -297,6 +298,24 @@ describe("agent definitions", () => { expect(() => loadAgent(agentName)).toThrow(/inference\.provider_type/); }); + it.each([ + "42", + '"bad model"', + ])("rejects invalid inference default models in manifests (%s)", (defaultModel) => { + const agentName = `invalid-inference-default-model-${String(Date.now())}-${defaultModel.length}`; + writeTempAgentManifest( + agentName, + [ + `name: ${agentName}`, + "display_name: Broken Inference Default", + "inference:", + ` default_model: ${defaultModel}`, + ].join("\n"), + ); + + expect(() => loadAgent(agentName)).toThrow(/inference\.default_model/); + }); + it("rejects invalid MCP bridge adapter declarations in manifests", () => { const agentName = `invalid-mcp-adapter-${String(Date.now())}`; writeTempAgentManifest( diff --git a/src/lib/agent/manifest-readers.ts b/src/lib/agent/manifest-readers.ts index 3961eff3954..45913a5d0e7 100644 --- a/src/lib/agent/manifest-readers.ts +++ b/src/lib/agent/manifest-readers.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import fs from "node:fs"; +import { isSafeModelId } from "../validation"; import type { AgentDashboard, AgentDashboardKind, @@ -261,7 +262,19 @@ export function readInference(record: ManifestRecord): AgentInference | undefine providerOptionList = providerOptions as string[]; } - return { provider_type: providerType, provider_options: providerOptionList }; + const defaultModel = inference.default_model; + if ( + defaultModel !== undefined && + (typeof defaultModel !== "string" || !isSafeModelId(defaultModel.trim())) + ) { + throw new Error("Agent manifest field 'inference.default_model' must be a safe model ID"); + } + + return { + provider_type: providerType, + provider_options: providerOptionList, + default_model: typeof defaultModel === "string" ? defaultModel.trim() : undefined, + }; } export function readMcpCapability(record: ManifestRecord): AgentMcpCapability { diff --git a/src/lib/inference/config.test.ts b/src/lib/inference/config.test.ts index 8078a8a4d64..66cae153001 100644 --- a/src/lib/inference/config.test.ts +++ b/src/lib/inference/config.test.ts @@ -22,12 +22,28 @@ import { OLLAMA_LOCAL_CREDENTIAL_ENV, parseGatewayInference, planInferenceRouteReconcile, + resolveAgentDefaultCloudModel, resolveAgentInferenceApi, resolveAgentProviderInferenceApi, sanitizeRouteValueForDisplay, VLLM_LOCAL_CREDENTIAL_ENV, } from "./config"; +describe("resolveAgentDefaultCloudModel", () => { + it("uses the Deep Agents manifest default without changing shared agent defaults", () => { + expect( + resolveAgentDefaultCloudModel({ + name: "langchain-deepagents-code", + inference: { default_model: "nvidia/nemotron-3-ultra-550b-a55b" }, + }), + ).toBe("nvidia/nemotron-3-ultra-550b-a55b"); + + for (const agent of [null, { name: "openclaw" }, { name: "hermes" }]) { + expect(resolveAgentDefaultCloudModel(agent)).toBe(DEFAULT_CLOUD_MODEL); + } + }); +}); + describe("resolveAgentInferenceApi", () => { it("uses the managed OpenAI frontend for Hermes custom Anthropic routes (#6289)", () => { expect( diff --git a/src/lib/inference/config.ts b/src/lib/inference/config.ts index 0f9ae6d8347..caf89f38935 100644 --- a/src/lib/inference/config.ts +++ b/src/lib/inference/config.ts @@ -6,7 +6,7 @@ * inference output parsing. All functions are pure. */ -import { shouldSkipResponsesProbe } from "../validation"; +import { isSafeModelId, shouldSkipResponsesProbe } from "../validation"; import { DEFAULT_OLLAMA_MODEL } from "./local"; export const INFERENCE_ROUTE_URL = "https://inference.local/v1"; @@ -68,6 +68,15 @@ export const VLLM_LOCAL_CREDENTIAL_ENV = "NEMOCLAW_VLLM_LOCAL_TOKEN"; export const MANAGED_PROVIDER_ID = "inference"; export { DEFAULT_OLLAMA_MODEL }; +/** Resolve an agent-owned NVIDIA Endpoints default without changing shared defaults. */ +export function resolveAgentDefaultCloudModel(agent: unknown): string { + const configured = (agent as { inference?: { default_model?: unknown } } | null | undefined) + ?.inference?.default_model; + return typeof configured === "string" && isSafeModelId(configured.trim()) + ? configured.trim() + : DEFAULT_CLOUD_MODEL; +} + export interface ProviderSelectionConfig { endpointType: string; endpointUrl: string; diff --git a/src/lib/onboard/nvidia-featured-model-selection.test.ts b/src/lib/onboard/nvidia-featured-model-selection.test.ts index e0f3a393102..bcfb4418311 100644 --- a/src/lib/onboard/nvidia-featured-model-selection.test.ts +++ b/src/lib/onboard/nvidia-featured-model-selection.test.ts @@ -12,8 +12,11 @@ vi.mock("../inference/model-prompts", () => ({ })); vi.mock("../inference/nvidia-featured-models", () => ({ - createNvidiaFeaturedModelPromptOptionsLoader: () => () => ({ - defaultModelId: "nvidia/nemotron-3-super-120b-a12b", + createNvidiaFeaturedModelPromptOptionsLoader: () => (defaultModelId?: string | null) => ({ + defaultModelId: + defaultModelId === "nvidia/nemotron-3-ultra-550b-a55b" + ? defaultModelId + : "nvidia/nemotron-3-super-120b-a12b", cloudModelOptions: [], }), })); @@ -26,7 +29,11 @@ describe("NVIDIA featured model selection", () => { it("propagates back navigation from the interactive model prompt (#5827)", async () => { vi.mocked(promptCloudModel).mockResolvedValueOnce(BACK_TO_SELECTION); - const selected = await createNvidiaFeaturedModelSession(vi.fn()).select(null, null, false); + const selected = await createNvidiaFeaturedModelSession({ writeLine: vi.fn() }).select( + null, + null, + false, + ); expect(selected).toBe(BACK_TO_SELECTION); }); @@ -34,7 +41,7 @@ describe("NVIDIA featured model selection", () => { it("preserves a custom environment model as the manual-entry default (#5827)", async () => { vi.mocked(promptCloudModel).mockResolvedValueOnce("custom/provider-model"); - const selected = await createNvidiaFeaturedModelSession(vi.fn()).select( + const selected = await createNvidiaFeaturedModelSession({ writeLine: vi.fn() }).select( null, null, false, @@ -48,4 +55,35 @@ describe("NVIDIA featured model selection", () => { manualDefaultModelId: "custom/provider-model", }); }); + + it("uses the agent default for interactive and non-interactive selection", async () => { + const ultra = "nvidia/nemotron-3-ultra-550b-a55b"; + vi.mocked(promptCloudModel).mockResolvedValueOnce(ultra); + const session = createNvidiaFeaturedModelSession({ + writeLine: vi.fn(), + defaultModel: ultra, + }); + + await expect(session.select(null, null, true)).resolves.toBe(ultra); + await expect(session.select(null, null, false)).resolves.toBe(ultra); + expect(promptCloudModel).toHaveBeenCalledWith({ + defaultModelId: ultra, + cloudModelOptions: [], + manualDefaultModelId: undefined, + }); + }); + + it("keeps requested, recovered, and environment models ahead of the agent default", async () => { + const session = createNvidiaFeaturedModelSession({ + defaultModel: "nvidia/nemotron-3-ultra-550b-a55b", + }); + + await expect(session.select("requested/model", "recovered/model", true)).resolves.toBe( + "requested/model", + ); + await expect(session.select(null, "recovered/model", true)).resolves.toBe("recovered/model"); + await expect(session.select(null, null, true, " environment/model ")).resolves.toBe( + "environment/model", + ); + }); }); diff --git a/src/lib/onboard/nvidia-featured-model-selection.ts b/src/lib/onboard/nvidia-featured-model-selection.ts index 0f46840b529..586fcba1519 100644 --- a/src/lib/onboard/nvidia-featured-model-selection.ts +++ b/src/lib/onboard/nvidia-featured-model-selection.ts @@ -15,24 +15,31 @@ export type NvidiaFeaturedModelSession = { ) => Promise; }; +export type NvidiaFeaturedModelSessionOptions = { + writeLine?: (message: string) => void; + defaultModel?: string; +}; + /** Create one catalog-backed model selector for an onboarding session. */ export function createNvidiaFeaturedModelSession( - writeLine: (message: string) => void = console.log, + options: NvidiaFeaturedModelSessionOptions = {}, ): NvidiaFeaturedModelSession { + const writeLine = options.writeLine ?? console.log; + const defaultModel = options.defaultModel?.trim() || DEFAULT_CLOUD_MODEL; const loadPromptOptions = createNvidiaFeaturedModelPromptOptionsLoader(); let announcedLoad = false; return { async select(requestedModel, recoveredModel, nonInteractive, envModel) { if (requestedModel) return requestedModel; if (recoveredModel) return recoveredModel; - if (nonInteractive) return DEFAULT_CLOUD_MODEL; + const configuredModel = envModel?.trim(); + if (nonInteractive) return configuredModel || defaultModel; if (!announcedLoad) { writeLine(" Loading NVIDIA's featured model catalog..."); announcedLoad = true; } - const configuredModel = envModel?.trim(); return promptCloudModel({ - ...loadPromptOptions(configuredModel), + ...loadPromptOptions(configuredModel || defaultModel), manualDefaultModelId: configuredModel, }); }, diff --git a/src/lib/onboard/setup-nim-flow.test.ts b/src/lib/onboard/setup-nim-flow.test.ts index c9d5ee31702..cdc1fdf40ad 100644 --- a/src/lib/onboard/setup-nim-flow.test.ts +++ b/src/lib/onboard/setup-nim-flow.test.ts @@ -134,6 +134,37 @@ afterEach(() => { }); describe("createSetupNim", () => { + it("passes the Deep Agents manifest default to NVIDIA model selection", async () => { + const ultra = "nvidia/nemotron-3-ultra-550b-a55b"; + const log = vi.fn(); + const createNvidiaFeaturedModelSession = vi.fn< + SetupNimFlowDeps["createNvidiaFeaturedModelSession"] + >(() => ({ select: async () => unexpected("featured model selection") })); + const handleRemoteProviderSelection = vi.fn( + async (_args, state) => { + state.model = ultra; + state.provider = "nvidia-prod"; + state.endpointUrl = "https://integrate.api.nvidia.com/v1"; + state.credentialEnv = "NVIDIA_INFERENCE_API_KEY"; + return "selected"; + }, + ); + const setupNim = createSetupNim( + makeDeps({ createNvidiaFeaturedModelSession, handleRemoteProviderSelection, log }), + ); + const dcodeAgent = { + name: "langchain-deepagents-code", + inference: { default_model: ultra }, + } as AgentDefinition; + + await setupNim(null, null, dcodeAgent); + + expect(createNvidiaFeaturedModelSession).toHaveBeenCalledWith({ + defaultModel: ultra, + writeLine: log, + }); + }); + it("announces detected Ollama but still prompts and defaults to NVIDIA Endpoints (#6245)", async () => { vi.stubEnv("NEMOCLAW_PROVIDER", ""); const step = vi.fn(); diff --git a/src/lib/onboard/setup-nim-flow.ts b/src/lib/onboard/setup-nim-flow.ts index cc122127a17..b419ea910a7 100644 --- a/src/lib/onboard/setup-nim-flow.ts +++ b/src/lib/onboard/setup-nim-flow.ts @@ -2,12 +2,18 @@ // SPDX-License-Identifier: Apache-2.0 import type { AgentDefinition } from "../agent/defs"; -import { resolveAgentProviderInferenceApi } from "../inference/config"; +import { + resolveAgentDefaultCloudModel, + resolveAgentProviderInferenceApi, +} from "../inference/config"; import type { VllmProfile } from "../inference/vllm"; import { isBackToSelection } from "../navigation"; import type { HermesAuthMethod } from "./hermes-auth"; import type { ProviderSelectionResult } from "./machine/handlers/provider-inference"; -import type { NvidiaFeaturedModelSession } from "./nvidia-featured-model-selection"; +import type { + NvidiaFeaturedModelSession, + NvidiaFeaturedModelSessionOptions, +} from "./nvidia-featured-model-selection"; import type { InferenceProviderHostGpu, InferenceProviderHostState } from "./provider-host-state"; import { buildInferenceProviderMenu, type ProviderMenuChoice } from "./provider-menu"; import { resolveRequestedProviderSelection } from "./provider-selection"; @@ -53,7 +59,9 @@ export interface SetupNimFlowDeps { isNonInteractive(): boolean; getNonInteractiveProvider(): string | null; getNonInteractiveModel(providerKey: string): string | null; - createNvidiaFeaturedModelSession(): NvidiaFeaturedModelSession; + createNvidiaFeaturedModelSession( + options?: NvidiaFeaturedModelSessionOptions, + ): NvidiaFeaturedModelSession; detectInferenceProviderHostState(input: { gpu: InferenceProviderHostGpu | null | undefined; experimental: boolean; @@ -193,7 +201,10 @@ export function createSetupNim( let compatibleEndpointReasoning: string | null = null; let allowToolsIncompatible = false; let reuseGatewayCredential = false; - const nvidiaFeaturedModels = deps.createNvidiaFeaturedModelSession(); + const nvidiaFeaturedModels = deps.createNvidiaFeaturedModelSession({ + defaultModel: resolveAgentDefaultCloudModel(agent), + writeLine: deps.log, + }); const providerHostState = deps.detectInferenceProviderHostState({ gpu, diff --git a/test/deepagents-code-tui-startup-check.test.ts b/test/deepagents-code-tui-startup-check.test.ts index d9b16b839ec..59656150423 100644 --- a/test/deepagents-code-tui-startup-check.test.ts +++ b/test/deepagents-code-tui-startup-check.test.ts @@ -53,12 +53,12 @@ function secretFixture(...parts: string[]): string { return parts.join(""); } -type TuiExpectEvent = "eof" | "exit" | "onboarding" | "ready" | "timeout"; +type TuiExpectEvent = "eof" | "exit" | "firstRun" | "ready" | "timeout"; const tclEventLiterals: Record = { eof: "{eof}", exit: "{exit}", - onboarding: "{onboarding}", + firstRun: "{firstRun}", ready: "{ready}", timeout: "{timeout}", }; @@ -104,9 +104,9 @@ proc expect {branches} { set event [lindex $::fake_events 0] set ::fake_events [lrange $::fake_events 1 end] switch -- $event { - onboarding { - set branch_index [lsearch -exact $branches {$onboarding_pattern}] - set ::expect_out(0,string) "Your name (optional)" + firstRun { + set branch_index [lsearch -exact $branches {$first_run_pattern}] + set ::expect_out(0,string) "Choose a Recommended Model" } ready { set branch_index [lsearch -exact $branches {$ready_pattern}] @@ -146,8 +146,8 @@ proc exit {{code 0}} { NEMOCLAW_TUI_CAPTURE: capture, NEMOCLAW_TUI_CLOSE_AFTER_FIRST_CTRL_C: options.closeAfterFirstCtrlC ? "1" : "0", NEMOCLAW_TUI_MARKERS: markers, - NEMOCLAW_TUI_ONBOARDING_PATTERN: - "(your name \\(optional\\)|what should deep agents call you)", + NEMOCLAW_TUI_FIRST_RUN_PATTERN: + "(your name \\(optional\\)|what should deep agents call you|choose a recommended model)", NEMOCLAW_TUI_READY_PATTERN: "(what would you like|enter (your )?(task|message|prompt)|how can i help)", NEMOCLAW_TUI_SANDBOX_NAME: "fake-deepagents", @@ -222,51 +222,30 @@ describe("Deep Agents Code TUI startup check helpers", () => { expect(readiness("How can I help with the codebase today?")).toBe("ready"); }); - it("matches only the pinned first-run onboarding name screen", () => { - const isOnboarding = (capture: string) => + it("matches the pinned first-run screens that managed DCode must suppress (#6410)", () => { + const isFirstRun = (capture: string) => runTuiStartupCheckHelper( - 'if printf "%s" "$CAPTURE" | grep -Eiq "$TUI_ONBOARDING_PATTERN"; then printf onboarding; else printf other; fi', + 'if printf "%s" "$CAPTURE" | grep -Eiq "$TUI_FIRST_RUN_PATTERN"; then printf first-run; else printf other; fi', { CAPTURE: capture }, ); - expect(isOnboarding("Your name (optional)")).toBe("onboarding"); - expect(isOnboarding("What should Deep Agents call you?")).toBe("onboarding"); - expect(isOnboarding("Your project name")).toBe("other"); - expect(isOnboarding("What would you like to build?")).toBe("other"); + expect(isFirstRun("Your name (optional)")).toBe("first-run"); + expect(isFirstRun("What should Deep Agents call you?")).toBe("first-run"); + expect(isFirstRun("Choose a Recommended Model")).toBe("first-run"); + expect(isFirstRun("Your project name")).toBe("other"); + expect(isFirstRun("What would you like to build?")).toBe("other"); }); - itWithTclsh("skips first-run onboarding before marking the real TUI prompt ready (tclsh)", () => { - const { markerText, result, traceText } = runTuiExpectStateMachine([ - "onboarding", - "ready", - "exit", - ]); + itWithTclsh("fails before readiness when a first-run model picker appears (#6410)", () => { + const { markerText, result, traceText } = runTuiExpectStateMachine(["firstRun"]); - expect(result.status, result.stderr).toBe(0); - expect(traceText).toBe("1b,03,03"); - expect(markerText).toContain("Your name (optional)"); - expect(markerText).toContain("What would you like to build?"); - expect(markerText).toContain("NEMOCLAW_TUI_ONBOARDING_SKIPPED"); - expect(markerText).toContain("NEMOCLAW_TUI_READY"); - expect(markerText.indexOf("NEMOCLAW_TUI_ONBOARDING_SKIPPED")).toBeLessThan( - markerText.indexOf("NEMOCLAW_TUI_READY"), - ); - expect(markerText).toContain("NEMOCLAW_TUI_EXIT_CAPTURED:0"); + expect(result.status, result.stderr).toBe(24); + expect(traceText).toBe("03"); + expect(markerText).toContain("Choose a Recommended Model"); + expect(markerText).toContain("NEMOCLAW_TUI_UNEXPECTED_FIRST_RUN"); + expect(markerText).not.toContain("NEMOCLAW_TUI_READY"); }); - itWithTclsh( - "does not mark the TUI ready when the coding prompt times out after onboarding (tclsh)", - () => { - const { markerText, result, traceText } = runTuiExpectStateMachine(["onboarding", "timeout"]); - - expect(result.status, result.stderr).toBe(20); - expect(traceText).toBe("1b,03"); - expect(markerText).toContain("NEMOCLAW_TUI_ONBOARDING_SKIPPED"); - expect(markerText).toContain("NEMOCLAW_TUI_TIMEOUT"); - expect(markerText).not.toContain("NEMOCLAW_TUI_READY"); - }, - ); - itWithTclsh("captures a clean exit when dcode closes after the first Ctrl-C (tclsh)", () => { const { markerText, result, traceText } = runTuiExpectStateMachine(["ready", "exit"], { closeAfterFirstCtrlC: true, @@ -312,7 +291,7 @@ describe("Deep Agents Code TUI startup check helpers", () => { "sandbox_exec() { printf 'NEMOCLAW_DCODE_PROBE:deepagents\\n'; }", "ensure_expect_available() { return 0; }", "run_tui_expect() {", - ' printf "Your name (optional)\\nNEMOCLAW_TUI_ONBOARDING_SKIPPED\\nWhat would you like to do next?\\nNEMOCLAW_TUI_READY\\nNEMOCLAW_TUI_EXIT_CAPTURED:130\\n" >>"$2"', + ' printf "What would you like to do next?\\nNEMOCLAW_TUI_READY\\nNEMOCLAW_TUI_EXIT_CAPTURED:130\\n" >>"$2"', " return 0", "}", "main", @@ -325,7 +304,6 @@ describe("Deep Agents Code TUI startup check helpers", () => { expect(result.stdout).toContain("finite expect harness reached startup and observed exit"); expect(result.stdout).toContain("dcode TUI rendered a usable startup prompt signature"); expect(result.stdout).toContain("dcode TUI exited cleanly after Ctrl-C (exit 130)"); - expect(sanitizedText).toContain("NEMOCLAW_TUI_ONBOARDING_SKIPPED"); expect(sanitizedText).toContain("NEMOCLAW_TUI_READY"); expect(sanitizedText).toContain("NEMOCLAW_TUI_EXIT_CAPTURED:130"); } finally { diff --git a/test/e2e/e2e-cloud-experimental/checks/10-deepagents-code-tui-startup.sh b/test/e2e/e2e-cloud-experimental/checks/10-deepagents-code-tui-startup.sh index 6f3b420f33e..15f56f1e9a6 100755 --- a/test/e2e/e2e-cloud-experimental/checks/10-deepagents-code-tui-startup.sh +++ b/test/e2e/e2e-cloud-experimental/checks/10-deepagents-code-tui-startup.sh @@ -23,11 +23,9 @@ CONTEXT_SECRET_VALUE_PATTERN='[A-Za-z0-9_.+\/=-]{10,}' # Upstream dcode does not expose a stable machine-readable TUI ready marker. # Keep this localized heuristic prompt-shaped; do not match banner-only text. TUI_READY_PATTERN='(what would you like|what do you want|enter (your )?(task|message|prompt)|describe (the )?(task|change)|how can i help)' -# New dcode homes enter a first-run modal before the coding prompt. Match only -# the pinned upstream name screen so Expect can take its documented skip path. -# deepagents-code 0.1.30 has no non-interactive first-run switch for its name screen. -# Remove this compatibility path once the pinned TUI exposes a stable skip or ready contract. -TUI_ONBOARDING_PATTERN='(your name \(optional\)|what should deep agents call you)' +# NemoClaw configures DCode's model and managed provider before launch, so any +# upstream first-run screen is a regression that can expose unusable providers. +TUI_FIRST_RUN_PATTERN='(your name \(optional\)|what should deep agents call you|choose a recommended model)' SENSITIVE_CAPTURE_FILES=() ok() { printf '%s\n' "${PREFIX}: OK ($*)"; } @@ -147,7 +145,7 @@ run_tui_expect() { env \ NEMOCLAW_TUI_CAPTURE="$raw_capture_file" \ NEMOCLAW_TUI_MARKERS="$marker_capture_file" \ - NEMOCLAW_TUI_ONBOARDING_PATTERN="$TUI_ONBOARDING_PATTERN" \ + NEMOCLAW_TUI_FIRST_RUN_PATTERN="$TUI_FIRST_RUN_PATTERN" \ NEMOCLAW_TUI_READY_PATTERN="$TUI_READY_PATTERN" \ NEMOCLAW_TUI_SANDBOX_NAME="$SANDBOX_NAME" \ NEMOCLAW_TUI_TIMEOUT="$TUI_TIMEOUT" \ @@ -156,7 +154,7 @@ set timeout $env(NEMOCLAW_TUI_TIMEOUT) set sandbox $env(NEMOCLAW_TUI_SANDBOX_NAME) set capture $env(NEMOCLAW_TUI_CAPTURE) set markers $env(NEMOCLAW_TUI_MARKERS) -set onboarding_pattern $env(NEMOCLAW_TUI_ONBOARDING_PATTERN) +set first_run_pattern $env(NEMOCLAW_TUI_FIRST_RUN_PATTERN) set ready_pattern $env(NEMOCLAW_TUI_READY_PATTERN) log_file -a $capture @@ -168,18 +166,17 @@ proc append_marker {markers marker} { set cmd [list openshell sandbox exec --name $sandbox --tty -- sh -lc {export TERM=xterm-256color; cd /sandbox; dcode; status=$?; printf "\nNEMOCLAW_TUI_EXIT:%s\n" "$status"}] spawn {*}$cmd -set saw_onboarding 0 set ready_match "" expect { -nocase -re $ready_pattern { set ready_match $expect_out(0,string) } - -nocase -re $onboarding_pattern { + -nocase -re $first_run_pattern { append_marker $markers "$expect_out(0,string)" - append_marker $markers "NEMOCLAW_TUI_ONBOARDING_SKIPPED" - puts "\nNEMOCLAW_TUI_ONBOARDING_SKIPPED" - send -- "\033" - set saw_onboarding 1 + append_marker $markers "NEMOCLAW_TUI_UNEXPECTED_FIRST_RUN" + puts "\nNEMOCLAW_TUI_UNEXPECTED_FIRST_RUN" + send -- "\003" + exit 24 } timeout { append_marker $markers "NEMOCLAW_TUI_TIMEOUT" @@ -194,25 +191,6 @@ expect { } } -if {$saw_onboarding} { - expect { - -nocase -re $ready_pattern { - set ready_match $expect_out(0,string) - } - timeout { - append_marker $markers "NEMOCLAW_TUI_TIMEOUT" - puts "\nNEMOCLAW_TUI_TIMEOUT" - send -- "\003" - exit 20 - } - eof { - append_marker $markers "NEMOCLAW_TUI_EOF_BEFORE_READY" - puts "\nNEMOCLAW_TUI_EOF_BEFORE_READY" - exit 21 - } - } -} - append_marker $markers "$ready_match" append_marker $markers "NEMOCLAW_TUI_READY" puts "\nNEMOCLAW_TUI_READY" diff --git a/test/langchain-deepagents-code-direct-module-patch.test.ts b/test/langchain-deepagents-code-direct-module-patch.test.ts index f14b4f79e86..9523060b615 100644 --- a/test/langchain-deepagents-code-direct-module-patch.test.ts +++ b/test/langchain-deepagents-code-direct-module-patch.test.ts @@ -141,6 +141,18 @@ def cli_main(): assert all(os.environ.get(name) == "false" for name in tracing_flags) assert os.environ["HOME"] == "/sandbox" print("managed-posture-ok") +`, + ); + writeFixtureFile( + packageDir, + "onboarding.py", + ` +from __future__ import annotations + + +def should_run_onboarding(state_dir=None): + del state_dir + return True `, ); writeFixtureFile( @@ -627,6 +639,7 @@ describe("LangChain Deep Agents Code managed package patch", () => { "widgets/auth.py", "widgets/codex_auth.py", "widgets/model_selector.py", + "onboarding.py", "widgets/approval.py", "server.py", "_server_config.py", @@ -659,6 +672,30 @@ describe("LangChain Deep Agents Code managed package patch", () => { } }); + it("skips the upstream first-run model picker in managed interactive sessions (#6410)", () => { + const tempDir = createPackageFixture(); + patchFixture(tempDir); + + const result = spawnSync( + "python3", + [ + "-c", + "from deepagents_code.onboarding import should_run_onboarding; print(should_run_onboarding())", + ], + { + env: { + PATH: process.env.PATH, + PYTHONPATH: tempDir, + DEEPAGENTS_CODE_DEBUG_ONBOARDING: "1", + }, + encoding: "utf8", + }, + ); + + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toBe("False\n"); + }); + it.each([ ["update"], ["auth"], diff --git a/test/langchain-deepagents-code-image.test.ts b/test/langchain-deepagents-code-image.test.ts index 1a157bf7274..d859dead56d 100644 --- a/test/langchain-deepagents-code-image.test.ts +++ b/test/langchain-deepagents-code-image.test.ts @@ -158,6 +158,7 @@ describe("LangChain Deep Agents Code image contracts", () => { const dockerfile = readAgentFile("Dockerfile"); expect(dockerfile).toContain("ARG BASE_IMAGE\n"); + expect(dockerfile).toContain("ARG NEMOCLAW_MODEL=nvidia/nemotron-3-ultra-550b-a55b"); expect(dockerfile).not.toContain("langchain-deepagents-code-sandbox-base:latest"); expect(dockerfile).toContain("chown root:root /sandbox/.nemoclaw"); expect(dockerfile).toContain("chmod 1755 /sandbox/.nemoclaw"); @@ -392,6 +393,7 @@ describe("LangChain Deep Agents Code image contracts", () => { expect(wrapper).toContain("extra_args=(--sandbox none --no-mcp)"); expect(managedRuntime).toContain(`_MCP_CONFIG_FILE = Path("${managedPath}")`); expect(patcher).toContain("managed_mcp_config = _nemoclaw_managed_mcp_config_path()"); + expect(patcher).toContain("def should_run_onboarding(state_dir=None) -> bool:"); expect(managedRuntime).toContain("if not servers:\n return None"); expect(managedRuntime).toContain("or descriptor != _MANAGED_MCP_FD"); expect(patcher).toContain("def discover_mcp_configs("); @@ -589,11 +591,11 @@ describe("LangChain Deep Agents Code image contracts", () => { "unable to probe sandbox", "unexpected sandbox probe output", "cd /sandbox; dcode", - 'NEMOCLAW_TUI_ONBOARDING_PATTERN="$TUI_ONBOARDING_PATTERN"', - "-nocase -re $onboarding_pattern", - 'append_marker $markers "NEMOCLAW_TUI_ONBOARDING_SKIPPED"', - 'send -- "\\033"', - "if {$saw_onboarding}", + 'NEMOCLAW_TUI_FIRST_RUN_PATTERN="$TUI_FIRST_RUN_PATTERN"', + "-nocase -re $first_run_pattern", + 'append_marker $markers "NEMOCLAW_TUI_UNEXPECTED_FIRST_RUN"', + "choose a recommended model", + "exit 24", 'send -- "\\003"\nafter 250\ncatch {send -- "\\003"}', 'append_marker $markers "$expect_out(0,string)"', 'append_marker $markers "NEMOCLAW_TUI_READY"', diff --git a/test/langchain-deepagents-code-progressive-tool-disclosure.test.ts b/test/langchain-deepagents-code-progressive-tool-disclosure.test.ts index ce2127c206f..6ed195b69ce 100644 --- a/test/langchain-deepagents-code-progressive-tool-disclosure.test.ts +++ b/test/langchain-deepagents-code-progressive-tool-disclosure.test.ts @@ -166,6 +166,10 @@ class CodexAuthScreen: class ModelSelectorScreen: def _select_with_auth_check(self, model_spec, provider): pass +`, + "onboarding.py": `from __future__ import annotations + +def should_run_onboarding(state_dir=None): return True `, "widgets/approval.py": `from __future__ import annotations