Skip to content

feat(projects): per-project default agent selection - #1834

Open
wgnrai wants to merge 4 commits into
agent0ai:mainfrom
wgnrai:feat/project-default-agent
Open

feat(projects): per-project default agent selection#1834
wgnrai wants to merge 4 commits into
agent0ai:mainfrom
wgnrai:feat/project-default-agent

Conversation

@wgnrai

@wgnrai wgnrai commented Aug 18, 2026

Copy link
Copy Markdown

Problem

Agent Zero has no per-project default agent. New chats always start on the global profile from usr/settings.json (agent_profile), and reconcile_agent_profile() only validates that the current profile is available in the project's agent catalog — it never selects a project-preferred agent. If the global profile is available in the project (the common case, since user-level agents are merged into every project), it simply stays selected.

For multi-project setups where each project has a dedicated orchestrator/entry agent (e.g. a per-project "captain"), users must manually select the right agent in every new chat. The project's preferred agent is never auto-selected.

Solution

Opt-in per-project default agent via a new config file:

// <project>/.a0proj/default_agent.json
{"agent": "my-project-orchestrator"}

Two changes in helpers/projects.py:

  1. New get_project_default_agent(name) — reads .a0proj/default_agent.json; returns None when absent, unreadable, or malformed (broad exception guard, feature fully inert).

  2. reconcile_agent_profile() extension — two behaviors:

    • When the context still runs the global default profile and the project default is available in the current scope, switch to the project default. Contexts running any other profile (i.e. explicit per-chat selections made via the agent dropdown / agent_profile_set) are never overridden.
    • The fallback branch (active profile missing from the available catalog, e.g. the global default is disabled in this project) now prefers the project default over agent0/first-in-dict. Ordering: project default → agent0 → first available.

Design decisions

  • Why a new file instead of extending existing project config: agents.json is normalized by _normalize_subagents() which only supports {profile: {enabled: bool}} — unknown keys/values are stripped or would break parsing. project.json passes through _normalizeBasicData/_normalizeEditData which build explicit key sets — unknown keys are dropped on the next WebUI project save. A dedicated file is read by exactly one new helper; nothing existing parses it.
  • Zero behavior change for projects without the file — the helper returns None and both code paths fall through to existing logic.
  • Manual selections are sacred: the switch only fires when context.config.profile == global settings agent_profile. After any explicit pick, the profile no longer matches the global default, so reconcile sweeps (project activation, chat creation, editor reconciliation) leave it alone. After a switch, the profile equals the project default (≠ global default), so later reconciles are stable — no oscillation.
  • The config swap mirrors the proven pattern in api/agent_profile_set.py (swap context.config + context.agent0.config; prompt rebuilds on next message).

Tests

New tests/test_project_default_agent.py — 6 scenarios:

  1. No default file → keeps global default profile (no switch)
  2. Default file present, context on global default → switches to project default
  3. Default file present, context on a different manually-selected profile → no switch (manual selection preserved)
  4. Default names an unavailable/disabled agent → ignored, existing behavior
  5. Malformed config file → treated as absent, no exception
  6. Global default disabled in project (fallback branch) → project default selected over agent0

Full suite: 26 passed (6 new + 20 existing test_projects.py regression, unchanged).

Deployment verification

Running in live deployment across 12 projects (each with a dedicated orchestrator agent) since 2026-08-18, including projects that disable the global default profile. No restart required for config-only changes — the file is read per reconciliation.

Files changed

  • helpers/projects.pyget_project_default_agent() + reconcile_agent_profile() extension
  • helpers/projects.py.dox.md — DOX contract updated (same-change requirement)
  • tests/test_project_default_agent.py — new, 6 scenarios

Add get_project_default_agent() reading opt-in .a0proj/default_agent.json,
and extend reconcile_agent_profile() to switch to the project default when
the context still runs the global default profile. Manual per-chat
selections are never overridden. The fallback branch also prefers the
project default over agent0/first-in-dict.

Projects without the config file behave exactly as before.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0f2ce2af1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread helpers/projects.py
from helpers import settings as settings_helper

global_default = settings_helper.get_settings().get("agent_profile", "")
if getattr(context.config, "profile", "") == global_default:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve explicit selections of the global profile

When a project default is configured and the user explicitly selects the global profile in that project, any later reconciliation sweep (for example project reactivation or agent availability changes) satisfies this equality and replaces that selection with default_agent. Since this check cannot distinguish a newly created chat from an explicit dropdown choice of the global profile, it makes that valid per-chat manual selection impossible to keep.

Useful? React with 👍 / 👎.

Comment thread helpers/projects.py Outdated
Comment on lines +445 to +446
if config.profile not in available:
fallback = "agent0" if "agent0" in available else next(iter(available), "agent0")
default_agent = get_project_default_agent(project_name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prefer the project default before accepting global fallback

When the current profile is unavailable but the global agent_profile is still available in the project, this branch accepts initialize_agent() and never reaches the new project-default fallback below. In that scenario, reconciling a chat whose previous profile was disabled/deleted lands on the global default instead of the configured project default, so the advertised fallback ordering only works when the global default is also disabled.

Useful? React with 👍 / 👎.

…oject default in fallback

- agent_profile_set now records agent_profile_manually_set on the context;
  reconcile honors it so explicit per-chat selections (including picking the
global profile) are never overridden by later reconciliation sweeps.
- fallback branch now prefers the configured project default even when the
global default is still available; ordering: project default, then agent0,
  then first available.
- tests 7-8 added covering both cases (8/8 green, 20/20 regression).
@wgnrai

wgnrai commented Aug 18, 2026

Copy link
Copy Markdown
Author

Both review findings addressed in 5235d4d:

  1. Preserve explicit selections of the global profileagent_profile_set now records an agent_profile_manually_set flag on the context (persisted through chat save/restore), and reconcile_agent_profile skips the default-agent switch whenever the flag is set. Explicit per-chat selections — including deliberately picking the global profile — are never overridden by later reconciliation sweeps.

  2. Prefer the project default before accepting global fallback — the fallback branch now resolves the project default up front and switches to it whenever it is configured, available, and differs from the global default — even when the global default is still available. Ordering is now: project default → agent0 → first available.

Tests 7-8 added covering both cases; suite is 8/8 + 20/20 regression green.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5235d4d0fc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread helpers/projects.py
Comment on lines +426 to +429
manually_set = context.get_data("agent_profile_manually_set")
default_agent = get_project_default_agent(project_name)
if (
not manually_set

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Mark every explicit profile choice before defaulting

Fresh evidence after the earlier review fix: only /agent_profile_set now writes agent_profile_manually_set, but other explicit selection paths still change context.config directly (for example /agent in helpers/integration_commands.py:469 and connector chat creation with an agent_profile before project activation). In a project whose default agent differs from the global profile, explicitly choosing the global profile through those paths leaves this flag false, so the next project activation/reconcile sweep satisfies this branch and silently switches the chat back to the project default instead of preserving the user's selection.

Useful? React with 👍 / 👎.

…aths

The /agent integration command and external API context creation with an
explicit agent_profile now record agent_profile_manually_set, matching
agent_profile_set. Without this, explicit selections through those paths
could still be overridden by project-default reconciliation. The api_message
flag is set before activate_project so the flag exists before reconcile
fires. Test 9 covers the /agent wiring end-to-end.
@wgnrai

wgnrai commented Aug 19, 2026

Copy link
Copy Markdown
Author

Round 2 findings addressed in d9123cc:

Both findings stemmed from the same root cause — explicit profile selection paths other than agent_profile_set did not record the manual-selection flag. Verified and fixed:

  1. /agent integration command (helpers/integration_commands.py) now records agent_profile_manually_set after the config swap, before save — explicit selections through the slash command are protected.
  2. External API context creation (api/api_message.py) records the flag when an explicit agent_profile is supplied — set before activate_project so the flag exists before the first reconcile fires.

Paths audited and deliberately unchanged: settings.py _apply_settings preserves each context existing profile (refresh sweep, not a selection); persist_chat restore path round-trips the flag through context.data serialization, so restored manual selections stay protected.

Test 9 added covering the /agent wiring end-to-end (switch lands, flag set via real set_data, subsequent reconcile returns False). Suite: 9/9 + 20/20 regression green.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d9123ccd39

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread helpers/projects.py
if getattr(context.config, "profile", "") in available:
# project default agent: switch only when the context still runs the
# global default (i.e. no explicit per-chat selection has been made)
manually_set = context.get_data("agent_profile_manually_set")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Set the manual flag for Telegram agent picks

When a project has .a0proj/default_agent.json and a user explicitly changes that chat back to the global profile from the Telegram inline agent picker, plugins/_telegram_integration/helpers/command_ui.py:423-426 still updates context.config/context.agent0.config and saves without writing agent_profile_manually_set. Because this new guard treats the missing flag as an implicit default selection, the next activation or reconcile sweep sees the chat on the global default and silently switches it back to the project default, losing the Telegram selection. Fresh evidence after the earlier fixes: /agent and API paths were updated in this patch, but the Telegram picker still has the unflagged config swap.

AGENTS.md reference: helpers/AGENTS.md:L15-L15

Useful? React with 👍 / 👎.

…nector chat creation

Completes manual-selection coverage across all five explicit profile
selection paths: agent_profile_set, /agent command, external API, Telegram
inline agent picker, and connector create_context. The connector flag is
set before activate_project so it exists before the first reconcile.
Test 11 covers the connector ordering end-to-end.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@wgnrai

wgnrai commented Aug 19, 2026

Copy link
Copy Markdown
Author

Round 3 findings addressed in 84faa3f:

The Telegram inline agent picker finding was correct — plugins/ contained two remaining unflagged explicit-selection paths my earlier audits (helpers/, api/, tools/) had missed. Both fixed:

  1. plugins/_telegram_integration/helpers/command_ui.py _select_agent now records agent_profile_manually_set after the config swap, before save — Telegram agent picks are protected from project-default reconciliation.
  2. plugins/_a0_connector/helpers/chat_context.py create_context records the flag when an explicit agent_profile is supplied — set immediately after context creation, before the inheritance block and activate_project, so the flag exists before the first reconcile fires.

Also audited and unchanged: the connector v1 agent_profile_set endpoint delegates to the core agent_profile_set handler (already flagged); _commands agent_profile references are scope keys, not selections; _migrate_agents references are settings migration.

Manual-selection coverage now spans all five explicit-selection paths: agent_profile_set, /agent command, external API, Telegram picker, connector create_context. Test 11 covers the connector ordering end-to-end (flag asserted True at the moment activate_project fires). Suite: 10/10 + 20/20 regression green.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant