Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/agent_profile_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ async def process(self, input: dict, request: Request) -> dict | Response:
config = initialize_agent(override_settings={"agent_profile": profile})
context.config = config
context.agent0.config = config
context.set_data("agent_profile_manually_set", True)

save_tmp_chat(context)
mark_dirty_for_context(context.id, reason="agent_profile_change")
Expand Down
1 change: 1 addition & 0 deletions api/agent_profile_set.py.dox.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- `SetAgentProfile` defines `process(...)`.
- Observed side-effect areas: filesystem writes, settings/state persistence.
- Switching a chat profile updates the context and top-level agent profile only; existing subordinate agents keep their own profile configs.
- Explicit profile selection sets the `agent_profile_manually_set` context data flag (persisted via `save_tmp_chat` because non-underscore `context.data` keys are serialized); `helpers/projects.py` `reconcile_agent_profile(...)` skips project-default-agent overrides for flagged contexts, so explicit selections survive reconcile sweeps.
- A profile can be selected only when it is available in the chat's active
project scope; profiles owned by other projects are not valid candidates.
- Imported dependency areas include: `agent`, `helpers`, `helpers.api`, `helpers.persist_chat`, `helpers.state_monitor_integration`.
Expand Down
2 changes: 2 additions & 0 deletions api/api_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ async def process(self, input: dict, request: Request) -> dict | Response:
context = AgentContext(config=config, type=AgentContextType.USER)
AgentContext.use(context.id)
context_id = context.id
if agent_profile:
context.set_data("agent_profile_manually_set", True)
# Activate project if provided
if project_name:
try:
Expand Down
1 change: 1 addition & 0 deletions api/api_message.py.dox.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
- `ApiMessage` defines `requires_csrf(...)`.
- `ApiMessage` defines `requires_api_key(...)`.
- Observed side-effect areas: filesystem reads, filesystem writes, settings/state persistence, secret handling, scheduler state.
- New-context creation with an explicit `agent_profile` records the `agent_profile_manually_set` context data flag (before project activation triggers reconciliation) so manual selections are protected from project-default reconciliation.
- Imported dependency areas include: `agent`, `base64`, `datetime`, `helpers`, `helpers.api`, `helpers.print_style`, `helpers.projects`, `helpers.security`, `initialize`, `os`, `uuid`.

## Key Concepts
Expand Down
1 change: 1 addition & 0 deletions helpers/integration_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ def _handle_agent(context: "AgentContext", args: str) -> str:
config = initialize_agent(override_settings={"agent_profile": profile})
context.config = config
context.agent0.config = config
context.set_data("agent_profile_manually_set", True)
save_tmp_chat(context)
mark_dirty_for_context(context.id, reason="integration_commands.agent_set")
return f"Switched agent to {match.get('label') or profile}."
Expand Down
3 changes: 3 additions & 0 deletions helpers/integration_commands.py.dox.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
- `/agent` switches the top-level chat profile and preserves existing
subordinate agent profiles. Choices come from the shared presentation
catalog, though status may report an existing chat using the utility profile.
- Explicit `/agent` selection records the `agent_profile_manually_set` context
data flag so manual selections are protected from project-default
reconciliation in `helpers/projects.py`.
- `/model <preset>` stores a per-chat global preset reference, `/model inherit` clears it, and status always reports the effective scoped-or-chat preset. `Default` is a real selectable preset, not an alias for clearing the chat selection.

## Key Concepts
Expand Down
48 changes: 46 additions & 2 deletions helpers/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,19 @@ def _get_projects_list(parent_dir):
return projects


def get_project_default_agent(name: str | None) -> str | None:
"""Return the project's preferred default agent profile, or None."""
if not name:
return None
try:
abs_path = files.get_abs_path(get_project_meta(name), "default_agent.json")
data = dirty_json.parse(files.read_file(abs_path))
agent = str(data.get("agent", "") or "").strip() if isinstance(data, dict) else ""
return agent or None
except Exception:
return None


def reconcile_agent_profile(
context: "AgentContext", project_name: str | None, available: dict | None = None
) -> bool:
Expand All @@ -408,11 +421,42 @@ def reconcile_agent_profile(
if available is None:
available = subagents.get_available_agents_dict(project_name)
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 👍 / 👎.

default_agent = get_project_default_agent(project_name)
if (
not manually_set
Comment on lines +426 to +429

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 👍 / 👎.

and default_agent
and default_agent in available
and default_agent != getattr(context.config, "profile", "")
):
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 👍 / 👎.

config = initialize_agent(
override_settings={"agent_profile": default_agent}
)
context.config = config
context.agent0.config = config
return True
return False

config = initialize_agent()
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)
if config.profile not in available or (
default_agent
and default_agent in available
and config.profile != default_agent
):
fallback = (
default_agent
if default_agent in available
else "agent0"
if "agent0" in available
else next(iter(available), "agent0")
)
config = initialize_agent(override_settings={"agent_profile": fallback})
context.config = config
context.agent0.config = config
Expand Down
14 changes: 14 additions & 0 deletions helpers/projects.py.dox.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
- `save_project_mcp_servers(name: str, mcp_servers: str)`
- `get_active_projects_list()`
- `_get_projects_list(parent_dir)`
- `get_project_default_agent(name: str | None) -> str | None`: Read the project's preferred default agent profile from `.a0proj/default_agent.json`; returns None when absent, unreadable, or malformed.
- `reconcile_agent_profile(context: AgentContext, project_name: str | None) -> bool`
- `reconcile_agent_profiles(project_name: str | None, *, all_scopes: bool=...) -> None`
- `activate_project(context_id: str, name: str, mark_dirty: bool=...)`
Expand Down Expand Up @@ -91,6 +92,18 @@
scope; only chats whose active profile actually changes are persisted and
marked dirty. Context creation uses the same reconciliation after resolving
its scope, so a disabled configured profile cannot become invisibly active.
- `reconcile_agent_profile(...)` additionally applies the per-project default
agent from `.a0proj/default_agent.json` (read via
`get_project_default_agent(...)`): when the context still runs the global
default profile and the configured default agent is available in the current
scope, the context switches to that agent. The switch is skipped entirely
when the context carries a truthy `agent_profile_manually_set` data flag
(set by `api/agent_profile_set.py` on explicit per-chat selection), so
manual selections are never overridden by reconcile sweeps. Projects
without the file behave exactly as before. The fallback branch (active
profile missing from the available catalog) prefers the project default
agent even over an available global default; fallback ordering is: project
default, then `agent0`, then first available.
- Project updates and deletion refresh only chats assigned to that project and
persist each affected chat once; unrelated chats are never rewritten.
- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, plugin state, settings/state persistence, secret handling.
Expand All @@ -112,6 +125,7 @@

- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
- Related tests observed by source search:
- `tests/test_project_default_agent.py`
- `tests/test_model_config_project_presets.py`
- `tests/test_office_document_store.py`
- `tests/test_plugin_activation_ui.py`
Expand Down
1 change: 1 addition & 0 deletions plugins/_a0_connector/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
after all chunks for the `op_id` are assembled.
- Host browser status metadata may advertise `available_browsers` entries with browser ids, labels, CDP endpoints, status, and enabled state; keep older CLI payloads without those fields compatible.
- Model preset definitions exposed through v1 are global; project arguments select scope but never create project-owned definitions. Model switcher state reports the effective main, utility, and embedding models and preserves embedding-change notifications.
- Context creation with an explicit `agent_profile` records the `agent_profile_manually_set` context flag before project activation so project-default reconciliation cannot override the selection.
- The protected v1 `agent_editor` route delegates to the bundled Agent Editor
API and must not define another profile schema or write profile files itself.
- The protected v1 `agents_list` response uses the shared agent presentation
Expand Down
3 changes: 3 additions & 0 deletions plugins/_a0_connector/helpers/chat_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ def create_context(
set_current=True,
)

if agent_profile:
context.set_data("agent_profile_manually_set", True)

if current_context and settings.get_settings().get("chat_inherit_project", True):
current_project = current_context.get_data(projects.CONTEXT_DATA_KEY_PROJECT)
if current_project:
Expand Down
2 changes: 2 additions & 0 deletions plugins/_telegram_integration/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
preserve existing subordinate agent profiles. Picker rows and direct matches
use the shared presentation catalog while current status may still report an
existing chat that uses the utility profile.
- Explicit agent picker selections record the `agent_profile_manually_set`
context flag so project-default reconciliation cannot override them.
- Model picker status shows the effective preset; clearing a chat override returns to its scoped preset rather than assuming `Default`.

## Work Guidance
Expand Down
1 change: 1 addition & 0 deletions plugins/_telegram_integration/helpers/command_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ async def _select_agent(context: AgentContext, index: int) -> None:
config = initialize_agent(override_settings={"agent_profile": profile})
context.config = config
context.agent0.config = config
context.set_data("agent_profile_manually_set", True)
save_tmp_chat(context)
mark_dirty_for_context(context.id, reason="telegram.agent_select")

Expand Down
Loading