diff --git a/agents/langchain-deepagents-code/patch-managed-deepagents-code.py b/agents/langchain-deepagents-code/patch-managed-deepagents-code.py index 75266fc457c..654fd9c223a 100644 --- a/agents/langchain-deepagents-code/patch-managed-deepagents-code.py +++ b/agents/langchain-deepagents-code/patch-managed-deepagents-code.py @@ -275,6 +275,29 @@ async def _nemoclaw_skip_launch_tavily(self) -> None: return None +async def _nemoclaw_skip_launch_model( + self, +) -> "tuple[bool, tuple[str, str] | None]": + """Skip model picker during first-run; NemoClaw owns model configuration.""" + return (False, None) + + +def _nemoclaw_skip_launch_dependencies_prompt(self): + """Return a pre-resolved dependency result that skips the model picker. + + The mount path pre-builds the model screen and passes it as + continue_screen to the name prompt, bypassing + _prompt_launch_dependencies_then_model. This override returns None + as the screen (so no model picker is pushed after the name prompt) + and a pre-resolved future with (False, None). + """ + import asyncio + loop = asyncio.get_running_loop() + result_future = loop.create_future() + result_future.set_result((False, None)) + return None, result_future + + async def _nemoclaw_block_model_auth(self, model_spec: str) -> bool: del model_spec self.notify(_NEMOCLAW_MANAGED_UI_MESSAGE, severity="warning", markup=False) @@ -316,6 +339,8 @@ def _nemoclaw_block_mcp_login(self, server_name: str) -> None: DeepAgentsApp.action_toggle_auto_approve = _nemoclaw_block_auto_approve DeepAgentsApp._set_rubric_model = _nemoclaw_block_rubric_model DeepAgentsApp._prompt_launch_tavily = _nemoclaw_skip_launch_tavily +DeepAgentsApp._prompt_launch_dependencies_then_model = _nemoclaw_skip_launch_model +DeepAgentsApp._build_launch_dependencies_prompt = _nemoclaw_skip_launch_dependencies_prompt DeepAgentsApp._prompt_model_auth_if_needed = _nemoclaw_block_model_auth DeepAgentsApp._show_auth_manager = _nemoclaw_block_auth_manager DeepAgentsApp._enter_service_api_key = _nemoclaw_block_service_key @@ -862,15 +887,6 @@ 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 { @@ -963,7 +979,6 @@ 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", @@ -1029,6 +1044,8 @@ def main() -> None: "_handle_update_action", "_handle_update_command", "_install_extra", + "_prompt_launch_dependencies_then_model", + "_build_launch_dependencies_prompt", "_prompt_launch_tavily", "_prompt_model_auth_if_needed", "_show_auth_manager", @@ -1101,9 +1118,6 @@ def main() -> None: "ModelSelectorScreen", {"_select_with_auth_check"}, ) - _require_functions( - paths["onboarding"], texts["onboarding"], {"should_run_onboarding"} - ) _require_methods( paths["approval"], texts["approval"], @@ -1178,9 +1192,6 @@ 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 7d4607269e1..f32feadbd60 100644 --- a/docs/get-started/quickstart-langchain-deepagents-code.mdx +++ b/docs/get-started/quickstart-langchain-deepagents-code.mdx @@ -41,7 +41,8 @@ This agent-specific default does not change the shared Nemotron 3 Super default 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. +Managed interactive sessions keep Deep Agents Code's optional first-run name prompt, skip its dependency and model selection screens, then open the TUI with the model selected during NemoClaw onboarding. +Press Enter at the name prompt to continue without setting a name. ## Choose the Default Sandbox diff --git a/test/deepagents-code-tui-startup-check.test.ts b/test/deepagents-code-tui-startup-check.test.ts index 59656150423..1ae3be367b5 100644 --- a/test/deepagents-code-tui-startup-check.test.ts +++ b/test/deepagents-code-tui-startup-check.test.ts @@ -53,12 +53,13 @@ function secretFixture(...parts: string[]): string { return parts.join(""); } -type TuiExpectEvent = "eof" | "exit" | "firstRun" | "ready" | "timeout"; +type TuiExpectEvent = "eof" | "exit" | "firstRun" | "namePrompt" | "ready" | "timeout"; const tclEventLiterals: Record = { eof: "{eof}", exit: "{exit}", firstRun: "{firstRun}", + namePrompt: "{namePrompt}", ready: "{ready}", timeout: "{timeout}", }; @@ -97,40 +98,60 @@ proc send {args} { set ::fake_closed 1 } } +proc exp_continue {} { + return -code continue +} proc expect {branches} { - if {[llength $::fake_events] == 0} { - error "fake Expect event queue exhausted" - } - set event [lindex $::fake_events 0] - set ::fake_events [lrange $::fake_events 1 end] - switch -- $event { - 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}] - set ::expect_out(0,string) "What would you like to build?" + while {1} { + if {[llength $::fake_events] == 0} { + error "fake Expect event queue exhausted" } - exit { - set branch_index [lsearch -glob $branches {NEMOCLAW_TUI_EXIT:*}] - set ::expect_out(0,string) "NEMOCLAW_TUI_EXIT:0" - set ::expect_out(1,string) "0" + set event [lindex $::fake_events 0] + set ::fake_events [lrange $::fake_events 1 end] + switch -- $event { + namePrompt { + set branch_index [lsearch -exact $branches {$name_prompt_pattern}] + set ::expect_out(0,string) "What should Deep Agents call you" + } + 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}] + set ::expect_out(0,string) "What would you like to build?" + } + exit { + set branch_index [lsearch -glob $branches {NEMOCLAW_TUI_EXIT:*}] + set ::expect_out(0,string) "NEMOCLAW_TUI_EXIT:0" + set ::expect_out(1,string) "0" + } + timeout { + set branch_index [lsearch -exact $branches timeout] + } + eof { + set branch_index [lsearch -exact $branches eof] + } + default { + error "unsupported fake Expect event: $event" + } } - timeout { - set branch_index [lsearch -exact $branches timeout] + if {$branch_index < 0} { + error "fake Expect event $event has no matching branch" } - eof { - set branch_index [lsearch -exact $branches eof] + set branch_result "" + set branch_options {} + set branch_code [catch { + uplevel 1 [lindex $branches [expr {$branch_index + 1}]] + } branch_result branch_options] + if {$branch_code == 0} { + return $branch_result } - default { - error "unsupported fake Expect event: $event" + if {$branch_code == 4} { + continue } + return -options $branch_options $branch_result } - if {$branch_index < 0} { - error "fake Expect event $event has no matching branch" - } - uplevel 1 [lindex $branches [expr {$branch_index + 1}]] } proc exit {{code 0}} { set trace_file [open $::env(NEMOCLAW_TUI_TRACE) w] @@ -146,8 +167,9 @@ 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_FIRST_RUN_PATTERN: - "(your name \\(optional\\)|what should deep agents call you|choose a recommended model)", + NEMOCLAW_TUI_FIRST_RUN_PATTERN: "(choose a recommended model)", + NEMOCLAW_TUI_NAME_PROMPT_PATTERN: + "(your name \\(optional\\)|what should deep agents call you)", NEMOCLAW_TUI_READY_PATTERN: "(what would you like|enter (your )?(task|message|prompt)|how can i help)", NEMOCLAW_TUI_SANDBOX_NAME: "fake-deepagents", @@ -222,20 +244,36 @@ describe("Deep Agents Code TUI startup check helpers", () => { expect(readiness("How can I help with the codebase today?")).toBe("ready"); }); - it("matches the pinned first-run screens that managed DCode must suppress (#6410)", () => { + it("matches the pinned first-run model picker that managed DCode must suppress (#6410)", () => { const isFirstRun = (capture: string) => runTuiStartupCheckHelper( 'if printf "%s" "$CAPTURE" | grep -Eiq "$TUI_FIRST_RUN_PATTERN"; then printf first-run; else printf other; fi', { CAPTURE: capture }, ); - 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 name (optional)")).toBe("other"); + expect(isFirstRun("What should Deep Agents call you?")).toBe("other"); expect(isFirstRun("Your project name")).toBe("other"); expect(isFirstRun("What would you like to build?")).toBe("other"); }); + it("matches the name prompt pattern that managed DCode allows on first run", () => { + const isNamePrompt = (capture: string) => + runTuiStartupCheckHelper( + 'if printf "%s" "$CAPTURE" | grep -Eiq "$TUI_NAME_PROMPT_PATTERN"; then printf name-prompt; else printf other; fi', + { + CAPTURE: capture, + TUI_NAME_PROMPT_PATTERN: "(your name \\(optional\\)|what should deep agents call you)", + }, + ); + + expect(isNamePrompt("Your name (optional)")).toBe("name-prompt"); + expect(isNamePrompt("What should Deep Agents call you?")).toBe("name-prompt"); + expect(isNamePrompt("Choose a Recommended Model")).toBe("other"); + expect(isNamePrompt("What would you like to build?")).toBe("other"); + }); + itWithTclsh("fails before readiness when a first-run model picker appears (#6410)", () => { const { markerText, result, traceText } = runTuiExpectStateMachine(["firstRun"]); @@ -246,6 +284,31 @@ describe("Deep Agents Code TUI startup check helpers", () => { expect(markerText).not.toContain("NEMOCLAW_TUI_READY"); }); + itWithTclsh("allows the first-run name prompt and proceeds to ready state", () => { + const { markerText, result, traceText } = runTuiExpectStateMachine( + ["namePrompt", "ready", "exit"], + { closeAfterFirstCtrlC: true }, + ); + + expect(result.status, result.stderr).toBe(0); + expect(traceText).toBe("0d,03"); + expect(markerText).toContain("What should Deep Agents call you"); + expect(markerText).toContain("NEMOCLAW_TUI_NAME_PROMPT"); + expect(markerText).toContain("NEMOCLAW_TUI_READY"); + expect(markerText).not.toContain("NEMOCLAW_TUI_UNEXPECTED_FIRST_RUN"); + }); + + itWithTclsh("still rejects the model picker when it appears after the name prompt", () => { + const { markerText, result, traceText } = runTuiExpectStateMachine(["namePrompt", "firstRun"]); + + expect(result.status, result.stderr).toBe(24); + expect(traceText).toBe("0d,03"); + expect(markerText).toContain("NEMOCLAW_TUI_NAME_PROMPT"); + expect(markerText).toContain("Choose a Recommended Model"); + expect(markerText).toContain("NEMOCLAW_TUI_UNEXPECTED_FIRST_RUN"); + 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, 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 15f56f1e9a6..2ff56c1b693 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,9 +23,10 @@ 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)' -# 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)' +# NemoClaw configures DCode's model and managed provider before launch, so +# the model picker is a regression. The name prompt is allowed on first run. +TUI_FIRST_RUN_PATTERN='(choose a recommended model)' +TUI_NAME_PROMPT_PATTERN='(your name \(optional\)|what should deep agents call you)' SENSITIVE_CAPTURE_FILES=() ok() { printf '%s\n' "${PREFIX}: OK ($*)"; } @@ -146,6 +147,7 @@ run_tui_expect() { NEMOCLAW_TUI_CAPTURE="$raw_capture_file" \ NEMOCLAW_TUI_MARKERS="$marker_capture_file" \ NEMOCLAW_TUI_FIRST_RUN_PATTERN="$TUI_FIRST_RUN_PATTERN" \ + NEMOCLAW_TUI_NAME_PROMPT_PATTERN="$TUI_NAME_PROMPT_PATTERN" \ NEMOCLAW_TUI_READY_PATTERN="$TUI_READY_PATTERN" \ NEMOCLAW_TUI_SANDBOX_NAME="$SANDBOX_NAME" \ NEMOCLAW_TUI_TIMEOUT="$TUI_TIMEOUT" \ @@ -155,6 +157,7 @@ set sandbox $env(NEMOCLAW_TUI_SANDBOX_NAME) set capture $env(NEMOCLAW_TUI_CAPTURE) set markers $env(NEMOCLAW_TUI_MARKERS) set first_run_pattern $env(NEMOCLAW_TUI_FIRST_RUN_PATTERN) +set name_prompt_pattern $env(NEMOCLAW_TUI_NAME_PROMPT_PATTERN) set ready_pattern $env(NEMOCLAW_TUI_READY_PATTERN) log_file -a $capture @@ -171,6 +174,14 @@ expect { -nocase -re $ready_pattern { set ready_match $expect_out(0,string) } + -nocase -re $name_prompt_pattern { + append_marker $markers "$expect_out(0,string)" + append_marker $markers "NEMOCLAW_TUI_NAME_PROMPT" + puts "\nNEMOCLAW_TUI_NAME_PROMPT" + # Pinned Deep Agents Code 0.1.30 accepts an empty optional name and continues. + send -- "\r" + exp_continue + } -nocase -re $first_run_pattern { append_marker $markers "$expect_out(0,string)" append_marker $markers "NEMOCLAW_TUI_UNEXPECTED_FIRST_RUN" diff --git a/test/fixtures/langchain-deepagents-code/app.py b/test/fixtures/langchain-deepagents-code/app.py index 801c2aceba4..e0e1072c7a5 100644 --- a/test/fixtures/langchain-deepagents-code/app.py +++ b/test/fixtures/langchain-deepagents-code/app.py @@ -86,6 +86,16 @@ async def _handle_auto_update_toggle(self): async def _prompt_launch_tavily(self): self.original_tavily = True + async def _prompt_launch_dependencies_then_model(self): + return (True, ("openai:gpt-4", "openai")) + + def _build_launch_dependencies_prompt(self): + import asyncio + loop = asyncio.get_running_loop() + fut = loop.create_future() + fut.set_result((True, ("openai:gpt-4", "openai"))) + return object(), fut + async def _prompt_model_auth_if_needed(self, model_spec): del model_spec return True diff --git a/test/langchain-deepagents-code-direct-module-patch.test.ts b/test/langchain-deepagents-code-direct-module-patch.test.ts index 9523060b615..d6e023f10ce 100644 --- a/test/langchain-deepagents-code-direct-module-patch.test.ts +++ b/test/langchain-deepagents-code-direct-module-patch.test.ts @@ -639,7 +639,6 @@ 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", @@ -672,30 +671,6 @@ 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"], @@ -1228,6 +1203,13 @@ async def validate(): assert instance._rubric_model is None assert instance._server_kwargs["rubric_model"] is None await instance._prompt_launch_tavily() + dep_continued, dep_result = await instance._prompt_launch_dependencies_then_model() + assert dep_continued is False + assert dep_result is None + dep_screen, dep_future = instance._build_launch_dependencies_prompt() + assert dep_screen is None + assert dep_future.done() + assert dep_future.result() == (False, None) assert await instance._prompt_model_auth_if_needed("provider:model") is False await instance._show_auth_manager(initial_provider="provider") await instance._enter_service_api_key(None, None) diff --git a/test/langchain-deepagents-code-image.test.ts b/test/langchain-deepagents-code-image.test.ts index d859dead56d..290565d50cd 100644 --- a/test/langchain-deepagents-code-image.test.ts +++ b/test/langchain-deepagents-code-image.test.ts @@ -393,7 +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(patcher).toContain("_nemoclaw_skip_launch_model"); expect(managedRuntime).toContain("if not servers:\n return None"); expect(managedRuntime).toContain("or descriptor != _MANAGED_MCP_FD"); expect(patcher).toContain("def discover_mcp_configs("); diff --git a/test/langchain-deepagents-code-progressive-tool-disclosure.test.ts b/test/langchain-deepagents-code-progressive-tool-disclosure.test.ts index 6ed195b69ce..ce1d5b13276 100644 --- a/test/langchain-deepagents-code-progressive-tool-disclosure.test.ts +++ b/test/langchain-deepagents-code-progressive-tool-disclosure.test.ts @@ -480,7 +480,8 @@ describe("Deep Agents 0.1.30 progressive-disclosure build patch", () => { expect(snapshot(managedPaths)).toEqual(firstBytes); for (const file of fixture.sourcePaths.filter( - (sourcePath) => !sourcePath.endsWith("/__init__.py"), + (sourcePath) => + !sourcePath.endsWith("/__init__.py") && !sourcePath.endsWith("/onboarding.py"), )) { expect( firstBytes[file].match(new RegExp(HARDENING_MARKER.replaceAll(".", "\\."), "g")), @@ -489,6 +490,10 @@ describe("Deep Agents 0.1.30 progressive-disclosure build patch", () => { expect( firstBytes[fixture.agentPath].match(/NemoClaw-managed progressive tool disclosure\./g), ).toHaveLength(1); + // Retain onboarding in the full-package snapshot to prove it stays untouched and idempotent. + expect(firstBytes[path.join(fixture.packageDir, "onboarding.py")]).not.toContain( + HARDENING_MARKER, + ); expect( firstBytes[fixture.agentPath].match(/ProgressiveToolDisclosureMiddleware\(\)/g), ).toHaveLength(2);