feat(projects): per-project default agent selection - #1834
Conversation
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.
There was a problem hiding this comment.
💡 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".
| from helpers import settings as settings_helper | ||
|
|
||
| global_default = settings_helper.get_settings().get("agent_profile", "") | ||
| if getattr(context.config, "profile", "") == global_default: |
There was a problem hiding this comment.
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 👍 / 👎.
| 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) |
There was a problem hiding this comment.
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).
|
Both review findings addressed in 5235d4d:
Tests 7-8 added covering both cases; suite is 8/8 + 20/20 regression green. |
There was a problem hiding this comment.
💡 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".
| manually_set = context.get_data("agent_profile_manually_set") | ||
| default_agent = get_project_default_agent(project_name) | ||
| if ( | ||
| not manually_set |
There was a problem hiding this comment.
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.
|
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:
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. |
There was a problem hiding this comment.
💡 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".
| 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") |
There was a problem hiding this comment.
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.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
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:
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. |
Problem
Agent Zero has no per-project default agent. New chats always start on the global profile from
usr/settings.json(agent_profile), andreconcile_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:
Two changes in
helpers/projects.py:New
get_project_default_agent(name)— reads.a0proj/default_agent.json; returnsNonewhen absent, unreadable, or malformed (broad exception guard, feature fully inert).reconcile_agent_profile()extension — two behaviors:agent_profile_set) are never overridden.agent0/first-in-dict. Ordering: project default →agent0→ first available.Design decisions
agents.jsonis normalized by_normalize_subagents()which only supports{profile: {enabled: bool}}— unknown keys/values are stripped or would break parsing.project.jsonpasses through_normalizeBasicData/_normalizeEditDatawhich 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.Noneand both code paths fall through to existing logic.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.api/agent_profile_set.py(swapcontext.config+context.agent0.config; prompt rebuilds on next message).Tests
New
tests/test_project_default_agent.py— 6 scenarios:agent0Full suite:
26 passed(6 new + 20 existingtest_projects.pyregression, 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.py—get_project_default_agent()+reconcile_agent_profile()extensionhelpers/projects.py.dox.md— DOX contract updated (same-change requirement)tests/test_project_default_agent.py— new, 6 scenarios