Skip to content

feat: select an agent profile for delegated automation work - #479

Merged
neubig merged 2 commits into
mainfrom
feat/automation-agent-profile-selection
Sep 16, 2026
Merged

neubig merged 2 commits into
mainfrom
feat/automation-agent-profile-selection

Conversation

@neubig

@neubig neubig commented Sep 16, 2026 •

Copy link
Copy Markdown
Member

Why

An automation that delegates work to an agent needs to select the Agent Server profile for that agent. The profile is the existing policy boundary for its model, tools, MCP servers, and secrets. Automation should only carry the profile identifier; profile resolution remains in Agent Server and the SDK.

The automation command itself remains an ordinary host-side scanner. When it delegates work, the merged Extensions workers pass the selected identifier to StartConversationRequest.agent_profile_id.

What changed

  • Add an optional agent_profile_id to automation definitions and their create, update, and response schemas.
  • Advertise agentProfiles in capabilities when Automation is connected to an Agent Server.
  • Reject selecting both the legacy automation model and an agent profile, because the profile owns model selection.
  • Export the selected identifier to the command as AUTOMATION_AGENT_PROFILE_ID.

The dispatcher reads the current automation definition, as it already does for model; this PR adds no run snapshot, preset-specific profile handling, profile resolver, secret filtering, Git synchronization, or conversation lifecycle machinery.

Closes #273.

Validation

  • uv run python -m pytest tests/test_db.py tests/test_dispatcher.py tests/test_router.py tests/test_capabilities_router.py -q — 233 passed.
  • uv run pre-commit run --all-files --show-diff-on-failure — passed.

Live evidence

A fresh Canvas selected profiles in the automation forms and retained them across reload. Scheduled developer and reviewer runs received the selected IDs, while an automation with no selected profile retained a null value. Six Docker developers ran concurrently, and Airbnb PR #87 completed development, review, and merge.

@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Coverage

Warning

Your comment is too long (maximum is 65536 characters), so the coverage report was not added. See the job log for how to reduce it.

@neubig

neubig commented Sep 16, 2026

Copy link
Copy Markdown
Member Author

@all-hands-bot please review this PR and approve it if no blocking findings remain.

@all-hands-bot

Copy link
Copy Markdown
Contributor

👋 This PR needs a couple of things fixed before OpenHands can review it:

  • the PR description's HUMAN: section needs at least 20 characters describing what you tested, not just the template placeholder

Push an update once this is addressed and this check re-runs automatically.

This is an automated check - no AI was used to generate this comment.

1 similar comment
@all-hands-bot

Copy link
Copy Markdown
Contributor

👋 This PR needs a couple of things fixed before OpenHands can review it:

  • the PR description's HUMAN: section needs at least 20 characters describing what you tested, not just the template placeholder

Push an update once this is addressed and this check re-runs automatically.

This is an automated check - no AI was used to generate this comment.

@all-hands-bot

Copy link
Copy Markdown
Contributor

PR Review: feat: select an agent profile for delegated automation work

I've reviewed the full diff (17 files, +250/�8) and run the tests that don't require Docker in this environment. Here's my assessment.

Summary

The PR adds an optional agent_profile_id to automation definitions and snapshots it on queued runs. The selected profile is mutually exclusive with model selection (the profile owns the model), is validated at every entry point (API, presets, git sync, capability preflight), and is exported to the automation command as AUTOMATION_AGENT_PROFILE_ID.

Detailed Review

Migration (025_add_agent_profile.py) � Clean. Adds a nullable agent_profile_id UUID column to both automations and automation_runs. Downgrade drops in reverse order (runs first, then automations). �

Models (models.py) � agent_profile_id on both Automation (the definition) and AutomationRun (the snapshot). Comments correctly explain that profile IDs belong to the configured Agent Server and that runs snapshot the value so definition edits only affect future runs. �

Validation (utils/model_profiles.py) � validate_agent_profile_selection enforces three rules:

  1. No-op when no profile is selected (backward compatible).
  2. Requires local mode (is_local_mode), which is true when agent_server_url is configured.
  3. Rejects specifying both a profile and a model (mutual exclusivity).

The lazy get_config import avoids a circular dependency. �

Router (router.py) � Create path validates then sets model=None when a profile is selected. Update path correctly falls back to auto.agent_profile_id when only model is in the update payload, clears model when a profile is active, and resolves model via resolve_model_profile_for_user when clearing the profile. �

Preset router (preset_router.py) � Both prompt and plugin creation paths follow the same validate-then-null-model pattern. The plugin path additionally rejects combining agent profiles with model experiment variants (422). �

Git sync (git_sync/loop.py, git_sync/serializer.py) � _validate_and_resolve_fields parses the UUID from YAML, validates it, and converts HTTPException to ValueError (consistent with all other validation in that function, which skips the directory on error). Serializer round-trips the profile ID as a string. �

Dispatcher (dispatcher.py) � Exports AUTOMATION_AGENT_PROFILE_ID as an env var only when the run has a profile. �

Run creation (utils/run.py, utils/webhook.py) � Both create_pending_run and create_automation_run snapshot automation.agent_profile_id onto the run at creation time. �

Capabilities router (capabilities_router.py) � Adds agentProfiles feature flag in local mode and validates the profile in the draft preflight endpoint. �

Schemas (schemas.py) � agent_profile_id added to CreateAutomationRequest, UpdateAutomationRequest, AutomationResponse, and AutomationRunResponse. All use uuid.UUID | None. �

Tests � Good coverage across all new paths:

  • test_profile_round_trip_and_queued_run_snapshot â�� create, patch (replace + clear), invalid UUID, and snapshot immutability after definition edit.
  • test_profile_import_uses_api_validation â�� git sync import with and without a conflicting model.
  • test_profile_reference_survives_git_round_trip â�� serializer round-trip.
  • test_selected_agent_profile_is_available_to_the_command â�� dispatcher env var export.
  • test_db.py â�� migration column assertions.

Test Results

I ran uv run pytest -q tests/test_router.py tests/test_git_sync_serializer.py tests/test_dispatcher.py in this environment. The 52 tests that don't require Docker passed. The 165 errors are all docker.errors.DockerException: Error while fetching server API version � a pre-existing environment limitation (no Docker socket available here), not related to this PR. The PR description confirms 217 tests pass in CI with Docker available.

Verdict

Approved. No blocking findings. The implementation is clean, follows existing patterns consistently across all entry points, enforces mutual exclusivity correctly, and has thorough test coverage. The snapshot-on-queue semantics are sound, and the migration is safe (nullable column, reversible).

Generated by OpenHands AI on behalf of the user.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved � no blocking findings. See the detailed review comment above.

@neubig

neubig commented Sep 16, 2026

Copy link
Copy Markdown
Member Author

@all-hands-bot please review the current head after the source-scoped secret lookup refactor. If there are no blocking findings, please explicitly approve this PR rather than leaving only a comment.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review: source-scoped secret lookup refactor (head c969997)

I reviewed the current head, focusing on the latest commit feat(profiles): scope automation secrets at lookup, and re-verified the full 3-commit diff against main.

What the refactor changes

Both preset entrypoints (presets/prompt/sdk_main.py and presets/plugin/sdk_main.py) now pass the selected profile ID to workspace.get_secrets():

secrets = workspace.get_secrets(
    agent_profile_id=os.environ.get("AUTOMATION_AGENT_PROFILE_ID")
)

When no profile is selected, os.environ.get() returns None, so the call is get_secrets(agent_profile_id=None) — fully backward compatible. The existing try/except wrapper means a mismatch with an older SDK that doesn't yet accept the argument degrades to a soft "no secrets" warning rather than a hard crash. os is already imported in both files.

Full PR assessment (unchanged from prior review, re-verified)

  • Migration 025: Nullable UUID column on automations and automation_runs; downgrade drops in reverse order. ✓
  • Models: agent_profile_id on both the definition and the run snapshot, with clear comments. ✓
  • Validation (validate_agent_profile_selection): No-op when None, requires local mode, rejects profile + model combination. Lazy get_config import avoids circular dependency. ✓
  • Router: Create nulls model when a profile is selected; update path correctly handles replace, clear, and model-only patches using update_data.get("agent_profile_id", auto.agent_profile_id). ✓
  • Preset router: Both prompt and plugin paths validate-then-null-model; plugin path additionally rejects profiles + experiment variants (422). ✓
  • Git sync: Parses UUID from YAML, validates via the shared helper, converts HTTPException to ValueError (consistent with all other field validation). Serializer round-trips as a string. ✓
  • Dispatcher: Exports AUTOMATION_AGENT_PROFILE_ID only when the run has a profile. ✓
  • Run creation: Both create_pending_run and create_automation_run snapshot the profile onto the run. ✓
  • Capabilities router: agentProfiles feature flag in local mode; profile validated in draft preflight. ✓
  • Schemas: agent_profile_id on all relevant request/response models. ✓

Test results

uv run pytest tests/test_preset_router.py tests/test_git_sync_serializer.py tests/test_db.py -q — 173 passed, 41 skipped. The test_router errors are all docker.errors.DockerException (no Docker socket in this environment), a pre-existing limitation unrelated to this PR. The PR description confirms 217 tests pass in CI with Docker available.

Verdict

Approved — no blocking findings. The source-scoped secret lookup refactor is a clean, minimal change that correctly threads the profile ID through to get_secrets() while preserving backward compatibility. The full PR is well-structured, consistently validated across all entry points, and thoroughly tested.

Generated by OpenHands AI on behalf of the user.

@neubig
neubig force-pushed the feat/automation-agent-profile-selection branch from c969997 to 5f93288 Compare September 16, 2026 15:23
@neubig
neubig requested a review from malhotra5 September 16, 2026 15:28
@neubig
neubig enabled auto-merge (squash) September 16, 2026 17:48
@neubig
neubig merged commit af5da33 into main Sep 16, 2026
9 checks passed
Comment on lines +531 to +535
agent_profile_id: uuid.UUID | None = Field(
default=None,
description="Agent profile selected for this automation.",
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dup code block

Comment on lines +52 to +53
if not get_config().service.is_local_mode:
raise HTTPException(422, "Agent profiles require a configured Agent Server")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this correct? Profiles won't work in remote deployments?

_build_event_payload(automation, run)
)
if automation.agent_profile_id:
env_vars["AUTOMATION_AGENT_PROFILE_ID"] = str(automation.agent_profile_id)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: followup would be to modify the sdk workspace to support workspace.get_agent_profile helper that will return the profile based off AUTOMATION_AGENT_PROFILE_ID

@openhands-release-bot openhands-release-bot Bot added the released: 1.13.0 Shipped in 1.13.0 label Sep 16, 2026
@openhands-release-bot

Copy link
Copy Markdown
Contributor

🚀 Released in 1.13.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

released: 1.13.0 Shipped in 1.13.0 type: feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow selecting an agent profile per automation (not just an LLM profile)

4 participants