Add `code_mode_native` tool metadata and `CodeMode(native_capabilities=...)` to keep tools out of `run_code`

MP
19d ago
+316-80

Why

CodeMode used code_arg_name (a trace-rendering hint) as its fold policy, users could only opt a capability's tools out by knowing their internal names, and Shell's background lifecycle was split between a native start_command and sandboxed check_command/stop_command (pydantic/pydantic-ai-harness#592, #593). This gives each concern its own knob, in the harness only, with no core change.

New public surface

  • Tool metadata key code_mode_native: True keeps a tool native whatever CodeMode(tools=...) selects; False lets the selector decide even when the tool carries code_arg_name. It is separate from the existing code_mode selection tag, so SetToolMetadata(code_mode=True) on every tool doesn't fold Shell.
  • CodeMode(native_capabilities=[FileSystem, ...]) (keyword-only): keeps every tool whose contributing capability, or one it wraps (PrefixTools, a resolved DynamicCapability), is an instance of a listed class. Uses ToolDefinition.capability_id + RunContext.capabilities.

User-visible behavior

Agent(model, capabilities=[LocalWorkspace('.'), Shell(), CodeMode()])
# before: run_code, run_command, start_command  (check/stop_command inside run_code)
# after:  run_code, run_command, start_command, check_command, stop_command

Shell, the ACP terminal, CapabilityCreation, and DynamicWorkflow now declare code_mode_native=True; code_arg_name stays only as the trace hint. clai2's speculative mode sets code_mode_native=False on shell instead of deleting code_arg_name, so folded shell calls keep code rendering in traces. Override someone else's declaration with SetToolMetadata(tools=['shell'], code_mode_native=False).

Verification

  • tests/harness/code_mode/test_code_mode.py: test_tool_declares_whether_it_stays_native (declared / undeclared code_arg_name / declared foldable, with tools naming the tool), test_native_capabilities_keep_a_capabilitys_tools_native (direct, PrefixTools, DynamicCapability), test_native_capabilities_rejects_names
  • tests/harness/shell/test_shell.py::TestCodeModeInterop (whole family native in both orders; metadata pinned per tool)
  • Existing ACP, CapabilityCreation, DynamicWorkflow, and clai2 speculative-mode tests pass unchanged apart from the metadata assertion.

What changes for existing users

Shell + CodeMode users now see check_command/stop_command as regular tool calls rather than functions inside run_code (issue problem 3); nothing else changes.

Scope and what's left

This is a first PR. Kept deliberately: a tool with code_arg_name and no code_mode_native key still stays native, so third-party code tools keep today's behavior; dropping that fallback (the issue's end state) is a follow-up decision. Not covered: Logfire rendering of code_arg_language: 'shell' (issue problem 4). native_capabilities takes classes, so it can't be set from an agent spec (rejected with a UserError); CodeMode's spec schema generation already fails on main because of os_access.

Checklist

  • No breaking changes in accordance with the version policy.
  • Any permitted compatibility impact has a label, warning, migration, release note, and exact API-check waiver.
  • PR title is fit for the release changelog.