Preserve subordinate agent profiles
Validate call_subordinate profile arguments against available agent profiles so missing profiles fail as repairable errors. Persist each agent's profile in saved chats and avoid flattening existing subordinate profiles during profile switches, settings refresh, or restart reload.
Alessandro committed
Jun 17, 2026 at 13:07 UTC
b9153f718ec883535faec598ad49ded31ca433bb
15 files changed
+353
-47
api/agent_profile_set.py
+2
-6
@@ -1,4 +1,4 @@
1
-from agent import Agent, AgentContext
1
+from agent import AgentContext
2
from helpers import subagents
3
from helpers.api import ApiHandler, Request, Response
4
from helpers.persist_chat import save_tmp_chat
@@ -39,11 +39,7 @@ class SetAgentProfile(ApiHandler):
39
40
config = initialize_agent(override_settings={"agent_profile": profile})
41
context.config = config
42
-
43
- agent = context.agent0
44
- while agent:
45
- agent.config = config
46
- agent = agent.get_data(Agent.DATA_NAME_SUBORDINATE)
42
+ context.agent0.config = config
43
44
save_tmp_chat(context)
45
mark_dirty_for_context(context.id, reason="agent_profile_change")
api/agent_profile_set.py.dox.md
+5
-3
@@ -23,11 +23,12 @@
23
- `SetAgentProfile` is an `ApiHandler`.
24
- `SetAgentProfile` defines `process(...)`.
25
- Observed side-effect areas: filesystem writes, settings/state persistence.
26
-- Imported dependency areas include: `agent`, `helpers`, `helpers.api`, `helpers.persist_chat`, `helpers.state_monitor_integration`, `initialize`.
26
+- Switching a chat profile updates the context and top-level agent profile only; existing subordinate agents keep their own profile configs.
27
+- Imported dependency areas include: `agent`, `helpers`, `helpers.api`, `helpers.persist_chat`, `helpers.state_monitor_integration`.
28
29
## Key Concepts
30
30
-- Important called helpers/classes observed in the source: `str.strip`, `context.is_running`, `_agent_profile_labels`, `initialize_agent`, `save_tmp_chat`, `mark_dirty_for_context`, `subagents.get_all_agents_list`, `Response`, `agent.get_data`.
31
+- Important called helpers/classes observed in the source: `str.strip`, `context.is_running`, `_agent_profile_labels`, `initialize_agent`, `context.agent0.config`, `save_tmp_chat`, `mark_dirty_for_context`, `subagents.get_all_agents_list`, `Response`.
32
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
33
34
## Work Guidance
@@ -39,7 +40,8 @@
40
## Verification
41
42
- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
42
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
43
+- Related tests observed by source search:
44
+ - `tests/test_subagent_profiles.py`
45
46
## Child DOX Index
47
helpers/integration_commands.py
+1
-5
@@ -436,7 +436,6 @@ def _handle_model(context: "AgentContext", args: str) -> str:
436
437
438
def _handle_agent(context: "AgentContext", args: str) -> str:
439
- from agent import Agent
439
from helpers import subagents
440
from initialize import initialize_agent
441
@@ -468,10 +467,7 @@ def _handle_agent(context: "AgentContext", args: str) -> str:
467
468
config = initialize_agent(override_settings={"agent_profile": profile})
469
context.config = config
471
- agent = context.agent0
472
- while agent:
473
- agent.config = config
474
- agent = agent.get_data(Agent.DATA_NAME_SUBORDINATE)
470
+ context.agent0.config = config
471
save_tmp_chat(context)
472
mark_dirty_for_context(context.id, reason="integration_commands.agent_set")
473
return f"Switched agent to {match.get('label') or profile}."
helpers/integration_commands.py.dox.md
+4
-2
@@ -31,10 +31,11 @@
31
- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
32
- Observed side-effect areas: filesystem writes, model calls, plugin state, settings/state persistence.
33
- Imported dependency areas include: `__future__`, `helpers`, `helpers.persist_chat`, `helpers.state_monitor_integration`, `plugins._model_config.helpers`, `re`, `typing`.
34
+- `/agent` switches the top-level chat profile and preserves existing subordinate agent profiles.
35
36
## Key Concepts
37
37
-- Important called helpers/classes observed in the source: `splitlines`, `extract_command_line`, `line.partition`, `command.strip.lower`, `parse_command`, `mq.get_queue`, `args.strip.lower`, `mq.send_all_aggregated`, `mark_dirty_for_context`, `_strip_quotes`, `_match_named_item`, `projects.activate_project`, `model_config.is_chat_override_allowed`, `context.get_data`, `context.set_data`, `save_tmp_chat`, `str.strip`, `value.strip`, `value.lower.strip`, `re.sub`.
38
+- Important called helpers/classes observed in the source: `splitlines`, `extract_command_line`, `line.partition`, `command.strip.lower`, `parse_command`, `mq.get_queue`, `args.strip.lower`, `mq.send_all_aggregated`, `mark_dirty_for_context`, `_strip_quotes`, `_match_named_item`, `projects.activate_project`, `initialize_agent`, `model_config.is_chat_override_allowed`, `context.get_data`, `context.set_data`, `save_tmp_chat`, `str.strip`, `value.strip`, `value.lower.strip`, `re.sub`.
39
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
40
41
## Work Guidance
@@ -46,7 +47,8 @@
47
## Verification
48
49
- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
49
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
50
+- Related tests observed by source search:
51
+ - `tests/test_subagent_profiles.py`
52
53
## Child DOX Index
54
helpers/persist_chat.py
+16
-3
@@ -132,8 +132,8 @@ def remove_msg_files(ctxid):
132
133
def _serialize_context(context: AgentContext):
134
profile = str(
135
- getattr(context.config, "profile", None)
136
- or getattr(context.agent0.config, "profile", None)
135
+ getattr(context.agent0.config, "profile", None)
136
+ or getattr(context.config, "profile", None)
137
or ""
138
)
139
@@ -180,6 +180,7 @@ def _serialize_agent(agent: Agent):
180
181
return {
182
"number": agent.number,
183
+ "agent_profile": str(getattr(agent.config, "profile", "") or ""),
184
"data": data,
185
"history": history,
186
}
@@ -232,11 +233,23 @@ def _deserialize_context(data):
233
streaming_agent = streaming_agent.data.get(Agent.DATA_NAME_SUBORDINATE, None)
234
235
context.agent0 = agent0
236
+ context.config = agent0.config
237
context.streaming_agent = streaming_agent
238
239
return context
240
241
242
+def _deserialize_agent_config(
243
+ agent_data: dict[str, Any], fallback_config: AgentConfig
244
+) -> AgentConfig:
245
+ fallback_profile = str(getattr(fallback_config, "profile", "") or "")
246
+ profile = str(agent_data.get("agent_profile") or fallback_profile).strip()
247
+ if profile == fallback_profile:
248
+ return fallback_config
249
+ override_settings = {"agent_profile": profile} if profile else None
250
+ return initialize_agent(override_settings=override_settings)
251
+
252
+
253
def _deserialize_agents(
254
agents: list[dict[str, Any]], config: AgentConfig, context: AgentContext
255
) -> Agent:
@@ -246,7 +259,7 @@ def _deserialize_agents(
259
for ag in agents:
260
current = Agent(
261
number=ag["number"],
249
- config=config,
262
+ config=_deserialize_agent_config(ag, config),
263
context=context,
264
)
265
current.data = ag.get("data", {})
helpers/persist_chat.py.dox.md
+5
-1
@@ -28,6 +28,7 @@
28
- `_serialize_agent(agent: Agent)`
29
- `_serialize_log(log: Log)`
30
- `_deserialize_context(data)`
31
+- `_deserialize_agent_config(agent_data: dict[str, Any], fallback_config: AgentConfig) -> AgentConfig`
32
- `_deserialize_agents(agents: list[dict[str, Any]], config: AgentConfig, context: AgentContext) -> Agent`
33
- `_deserialize_log(data: dict[str, Any]) -> 'Log'`
34
- `_safe_json_serialize(obj, **kwargs)`
@@ -39,10 +40,12 @@
40
- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
41
- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, settings/state persistence, scheduler state.
42
- Imported dependency areas include: `agent`, `collections`, `datetime`, `helpers`, `helpers.localization`, `helpers.log`, `initialize`, `json`, `typing`, `uuid`.
43
+- Serialized chats store `agent_profile` both at the context level for the main chat and on each serialized agent so subordinate profiles survive server restart.
44
+- Deserialization must rebuild each agent with its serialized profile when present, falling back to the context profile for older chat files.
45
46
## Key Concepts
47
45
-- Important called helpers/classes observed in the source: `datetime.fromtimestamp.isoformat`, `datetime.fromisoformat`, `files.get_abs_path`, `_get_chat_file_path`, `files.make_dirs`, `_serialize_context`, `_safe_json_serialize`, `files.write_file`, `_convert_v080_chats`, `files.list_files`, `get_chat_folder_path`, `files.delete_dir`, `get_chat_msg_files_folder`, `agent.history.serialize`, `initialize_agent`, `_deserialize_log`, `AgentContext`, `_deserialize_agents`, `Log`, `log.set_initial_progress`.
48
+- Important called helpers/classes observed in the source: `datetime.fromtimestamp.isoformat`, `datetime.fromisoformat`, `files.get_abs_path`, `_get_chat_file_path`, `files.make_dirs`, `_serialize_context`, `_safe_json_serialize`, `files.write_file`, `_convert_v080_chats`, `files.list_files`, `get_chat_folder_path`, `files.delete_dir`, `get_chat_msg_files_folder`, `agent.history.serialize`, `initialize_agent`, `_deserialize_log`, `AgentContext`, `_deserialize_agent_config`, `_deserialize_agents`, `Log`, `log.set_initial_progress`.
49
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
50
51
## Work Guidance
@@ -58,6 +61,7 @@
61
- `tests/test_api_chat_lifetime.py`
62
- `tests/test_browser_agent_regressions.py`
63
- `tests/test_persist_chat_log_ids.py`
64
+ - `tests/test_subagent_profiles.py`
65
- `tests/test_tool_action_contracts.py`
66
67
## Child DOX Index
helpers/settings.py
+16
-7
@@ -572,18 +572,27 @@ def _apply_settings(previous: Settings | None, browser_timezone: str | None = No
572
if _settings:
573
_apply_timezone_setting(previous, browser_timezone)
574
575
- from agent import AgentContext
575
+ from agent import Agent, AgentContext
576
from initialize import initialize_agent
577
578
for ctx in AgentContext.all():
579
- profile = str(getattr(ctx.config, "profile", "") or _settings["agent_profile"])
580
- config = initialize_agent(override_settings={"agent_profile": profile})
581
- ctx.config = config # reinitialize context config with new settings
582
- # apply config to agents
579
+ profile = str(
580
+ getattr(ctx.config, "profile", "") or _settings["agent_profile"]
581
+ )
582
+ ctx.config = initialize_agent(override_settings={"agent_profile": profile})
583
agent = ctx.agent0
584
while agent:
585
- agent.config = ctx.config
586
- agent = agent.get_data(agent.DATA_NAME_SUBORDINATE)
585
+ agent_profile = str(
586
+ getattr(getattr(agent, "config", None), "profile", "") or profile
587
+ )
588
+ agent.config = (
589
+ ctx.config
590
+ if agent is ctx.agent0 and agent_profile == profile
591
+ else initialize_agent(
592
+ override_settings={"agent_profile": agent_profile}
593
+ )
594
+ )
595
+ agent = agent.get_data(Agent.DATA_NAME_SUBORDINATE)
596
597
# update mcp settings if necessary
598
if not previous or _settings["mcp_servers"] != previous["mcp_servers"]:
helpers/settings.py.dox.md
+3
-1
@@ -61,7 +61,8 @@
61
62
## Key Concepts
63
64
-- Important called helpers/classes observed in the source: `TypeVar`, `files.get_abs_path`, `dotenv.get_dotenv_value`, `opts.insert`, `str.strip`, `_is_valid_timezone`, `str.strip.lower`, `_normalize_timezone_setting`, `SettingsOutput`, `get_default_settings`, `_ensure_option_present`, `_resolve_runtime_timezone`, `get_default_secrets_manager`, `get_settings`, `normalize_settings`, `_load_sensitive_settings`, `settings.copy`, `_write_settings_file`, `reload_settings`, `set_settings`.
64
+- Important called helpers/classes observed in the source: `TypeVar`, `files.get_abs_path`, `dotenv.get_dotenv_value`, `opts.insert`, `str.strip`, `_is_valid_timezone`, `str.strip.lower`, `_normalize_timezone_setting`, `SettingsOutput`, `get_default_settings`, `_ensure_option_present`, `_resolve_runtime_timezone`, `get_default_secrets_manager`, `get_settings`, `normalize_settings`, `_load_sensitive_settings`, `settings.copy`, `_write_settings_file`, `reload_settings`, `set_settings`, `initialize_agent`.
65
+- Applying settings refreshes active context configs while preserving each subordinate agent's own profile.
66
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
67
68
## Work Guidance
@@ -82,6 +83,7 @@
83
- `tests/test_model_config_api_keys.py`
84
- `tests/test_model_config_project_presets.py`
85
- `tests/test_oauth_static.py`
86
+ - `tests/test_subagent_profiles.py`
87
88
## Child DOX Index
89
helpers/subagents.py
+1
-1
@@ -433,4 +433,4 @@ def get_paths(
433
434
435
# end-of-file imports to prevent circular imports
436
-from helpers import plugins
\ No newline at end of file
436
+from helpers import plugins
plugins/_telegram_integration/AGENTS.md
+1
@@ -16,6 +16,7 @@
16
- Treat bot tokens, chat IDs, attachments, and user data as sensitive.
17
- Keep allowed-user, group-mode, project, model, and `/send` controls enforced.
18
- Install Telegram dependencies into the framework runtime only when required.
19
+- Agent profile picker actions change the top-level chat profile and must preserve existing subordinate agent profiles.
20
21
## Work Guidance
22
plugins/_telegram_integration/helpers/command_ui.py
+1
-5
@@ -413,7 +413,6 @@ async def _select_project(context: AgentContext, index: int, *, clear: bool = Fa
413
async def _select_agent(context: AgentContext, index: int) -> None:
414
if context.is_running():
415
return
416
- from agent import Agent
416
from initialize import initialize_agent
417
418
items = [item for item in subagents.get_all_agents_list() if item.get("key")]
@@ -422,10 +421,7 @@ async def _select_agent(context: AgentContext, index: int) -> None:
421
profile = str(items[index]["key"])
422
config = initialize_agent(override_settings={"agent_profile": profile})
423
context.config = config
425
- agent = context.agent0
426
- while agent:
427
- agent.config = config
428
- agent = agent.get_data(Agent.DATA_NAME_SUBORDINATE)
424
+ context.agent0.config = config
425
save_tmp_chat(context)
426
mark_dirty_for_context(context.id, reason="telegram.agent_select")
427
prompts/agent.system.tool.call_sub.md
+1
-1
@@ -1,7 +1,7 @@
1
### call_subordinate
2
delegate research or complex subtasks to a specialized agent.
3
args: `message`, optional `profile`, `reset`
4
-- `profile`: optional prompt profile name for the subordinate; leave empty for the default profile
4
+- `profile`: optional prompt profile key for the subordinate; when provided, it must exactly match an available profile; leave empty for the default profile
5
- `reset`: use json boolean `true` for the first message or when changing profile; use `false` to continue
6
- `message`: define role, goal, and the concrete task
7
after the subordinate returns, answer from its result directly when it satisfies the user request
tests/test_subagent_profiles.py
new
+234
@@ -0,0 +1,234 @@
1
+from __future__ import annotations
2
+
3
+from types import SimpleNamespace
4
+
5
+import pytest
6
+
7
+from agent import Agent, AgentConfig, AgentContext
8
+from helpers import persist_chat
9
+from helpers.errors import RepairableException
10
+
11
+
12
+class _FakeContext:
13
+ id = "ctx"
14
+
15
+ def get_data(self, key: str, recursive: bool = True):
16
+ return None
17
+
18
+
19
+class _FakeParentAgent:
20
+ def __init__(self) -> None:
21
+ self.number = 0
22
+ self.agent_name = "A0"
23
+ self.config = AgentConfig(mcp_servers="", profile="agent0")
24
+ self.context = _FakeContext()
25
+ self.data = {}
26
+
27
+ def get_data(self, key: str):
28
+ return self.data.get(key)
29
+
30
+ def set_data(self, key: str, value):
31
+ self.data[key] = value
32
+
33
+ def read_prompt(self, _file: str, **_kwargs) -> str:
34
+ return ""
35
+
36
+
37
+class _FakeSubAgent:
38
+ DATA_NAME_SUPERIOR = "_superior"
39
+ DATA_NAME_SUBORDINATE = "_subordinate"
40
+
41
+ def __init__(self, number: int, config: AgentConfig, context) -> None:
42
+ self.number = number
43
+ self.config = config
44
+ self.context = context
45
+ self.data = {}
46
+ self.history = SimpleNamespace(new_topic=lambda: None)
47
+ self.messages = []
48
+
49
+ def set_data(self, key: str, value):
50
+ self.data[key] = value
51
+
52
+ def hist_add_user_message(self, message):
53
+ self.messages.append(message)
54
+
55
+ async def monologue(self):
56
+ return "delegated"
57
+
58
+
59
+@pytest.mark.asyncio
60
+async def test_call_subordinate_rejects_unknown_profile(monkeypatch) -> None:
61
+ import tools.call_subordinate as call_subordinate
62
+
63
+ monkeypatch.setattr(
64
+ call_subordinate,
65
+ "_subordinate_profile_labels",
66
+ lambda _agent: {"developer": "Developer", "researcher": "Researcher"},
67
+ )
68
+ parent = _FakeParentAgent()
69
+ tool = call_subordinate.Delegation(
70
+ parent, # type: ignore[arg-type]
71
+ "call_subordinate",
72
+ None,
73
+ {"profile": "ghost", "message": "work"},
74
+ "",
75
+ None,
76
+ )
77
+
78
+ with pytest.raises(RepairableException, match="Agent profile 'ghost' not found"):
79
+ await tool.execute(message="work", profile="ghost", reset=True)
80
+
81
+ assert parent.data == {}
82
+
83
+
84
+@pytest.mark.asyncio
85
+async def test_call_subordinate_uses_valid_profile(monkeypatch) -> None:
86
+ import tools.call_subordinate as call_subordinate
87
+
88
+ monkeypatch.setattr(call_subordinate, "Agent", _FakeSubAgent)
89
+ monkeypatch.setattr(
90
+ call_subordinate,
91
+ "_subordinate_profile_labels",
92
+ lambda _agent: {"developer": "Developer"},
93
+ )
94
+ monkeypatch.setattr(
95
+ call_subordinate,
96
+ "initialize_agent",
97
+ lambda override_settings=None: AgentConfig(
98
+ mcp_servers="",
99
+ profile=(override_settings or {}).get("agent_profile", "agent0"),
100
+ ),
101
+ )
102
+
103
+ parent = _FakeParentAgent()
104
+ tool = call_subordinate.Delegation(
105
+ parent, # type: ignore[arg-type]
106
+ "call_subordinate",
107
+ None,
108
+ {"profile": "developer", "message": "work"},
109
+ "",
110
+ None,
111
+ )
112
+
113
+ response = await tool.execute(message="work", profile="developer", reset=True)
114
+ child = parent.get_data(_FakeSubAgent.DATA_NAME_SUBORDINATE)
115
+
116
+ assert response.message == "delegated"
117
+ assert child.config.profile == "developer"
118
+ assert child.messages[0].message == "work"
119
+
120
+
121
+@pytest.mark.asyncio
122
+async def test_call_subordinate_requires_reset_to_change_existing_profile(monkeypatch) -> None:
123
+ import tools.call_subordinate as call_subordinate
124
+
125
+ monkeypatch.setattr(
126
+ call_subordinate,
127
+ "_subordinate_profile_labels",
128
+ lambda _agent: {"developer": "Developer", "researcher": "Researcher"},
129
+ )
130
+
131
+ parent = _FakeParentAgent()
132
+ existing = SimpleNamespace(config=AgentConfig(mcp_servers="", profile="developer"))
133
+ parent.set_data(_FakeSubAgent.DATA_NAME_SUBORDINATE, existing)
134
+ tool = call_subordinate.Delegation(
135
+ parent, # type: ignore[arg-type]
136
+ "call_subordinate",
137
+ None,
138
+ {"profile": "researcher", "message": "work"},
139
+ "",
140
+ None,
141
+ )
142
+
143
+ with pytest.raises(RepairableException, match="Set reset=true"):
144
+ await tool.execute(message="work", profile="researcher", reset=False)
145
+
146
+
147
+def test_persist_chat_roundtrip_preserves_each_agent_profile(monkeypatch) -> None:
148
+ monkeypatch.setattr(
149
+ persist_chat,
150
+ "initialize_agent",
151
+ lambda override_settings=None: AgentConfig(
152
+ mcp_servers="",
153
+ profile=(override_settings or {}).get("agent_profile", "agent0"),
154
+ ),
155
+ )
156
+
157
+ context_id = "ctx-subagent-profile"
158
+ AgentContext.remove(context_id)
159
+ context = AgentContext(
160
+ config=AgentConfig(mcp_servers="", profile="agent0"),
161
+ id=context_id,
162
+ set_current=False,
163
+ )
164
+ child = Agent(1, AgentConfig(mcp_servers="", profile="developer"), context)
165
+ context.agent0.set_data(Agent.DATA_NAME_SUBORDINATE, child)
166
+ child.set_data(Agent.DATA_NAME_SUPERIOR, context.agent0)
167
+
168
+ try:
169
+ serialized = persist_chat._serialize_context(context)
170
+ assert serialized["agent_profile"] == "agent0"
171
+ assert serialized["agents"][0]["agent_profile"] == "agent0"
172
+ assert serialized["agents"][1]["agent_profile"] == "developer"
173
+
174
+ AgentContext.remove(context_id)
175
+ restored = persist_chat._deserialize_context(serialized)
176
+ restored_child = restored.agent0.get_data(Agent.DATA_NAME_SUBORDINATE)
177
+
178
+ assert restored.config.profile == "agent0"
179
+ assert restored.agent0.config.profile == "agent0"
180
+ assert restored_child.config.profile == "developer"
181
+ finally:
182
+ AgentContext.remove(context_id)
183
+
184
+
185
+@pytest.mark.asyncio
186
+async def test_agent_profile_set_preserves_subagent_profile(monkeypatch) -> None:
187
+ import api.agent_profile_set as agent_profile_set
188
+
189
+ monkeypatch.setattr(
190
+ agent_profile_set,
191
+ "_agent_profile_labels",
192
+ lambda: {"researcher": "Researcher"},
193
+ )
194
+ monkeypatch.setattr(
195
+ agent_profile_set,
196
+ "initialize_agent",
197
+ lambda override_settings=None: AgentConfig(
198
+ mcp_servers="",
199
+ profile=(override_settings or {}).get("agent_profile", "agent0"),
200
+ ),
201
+ )
202
+ monkeypatch.setattr(agent_profile_set, "save_tmp_chat", lambda _context: None)
203
+ monkeypatch.setattr(
204
+ agent_profile_set,
205
+ "mark_dirty_for_context",
206
+ lambda *_args, **_kwargs: None,
207
+ )
208
+
209
+ context_id = "ctx-profile-switch"
210
+ AgentContext.remove(context_id)
211
+ context = AgentContext(
212
+ config=AgentConfig(mcp_servers="", profile="agent0"),
213
+ id=context_id,
214
+ set_current=False,
215
+ )
216
+ child = Agent(1, AgentConfig(mcp_servers="", profile="developer"), context)
217
+ context.agent0.set_data(Agent.DATA_NAME_SUBORDINATE, child)
218
+ child.set_data(Agent.DATA_NAME_SUPERIOR, context.agent0)
219
+
220
+ try:
221
+ handler = agent_profile_set.SetAgentProfile.__new__(
222
+ agent_profile_set.SetAgentProfile
223
+ )
224
+ response = await handler.process(
225
+ {"context_id": context_id, "agent_profile": "researcher"},
226
+ request=None, # type: ignore[arg-type]
227
+ )
228
+
229
+ assert response["ok"] is True
230
+ assert context.config.profile == "researcher"
231
+ assert context.agent0.config.profile == "researcher"
232
+ assert child.config.profile == "developer"
233
+ finally:
234
+ AgentContext.remove(context_id)
tools/call_subordinate.py
+55
-10
@@ -1,26 +1,71 @@
1
from agent import Agent, UserMessage
2
+from helpers import projects, subagents
3
+from helpers.errors import RepairableException
4
from helpers.tool import Tool, Response
5
from initialize import initialize_agent
6
from extensions.python.hist_add_tool_result import _90_save_tool_call_file as save_tool_call_file
7
8
9
+def _subordinate_profile_labels(agent: Agent) -> dict[str, str]:
10
+ project = projects.get_context_project_name(agent.context) if agent.context else None
11
+ return {
12
+ name: subagent.title or name
13
+ for name, subagent in subagents.get_available_agents_dict(project).items()
14
+ }
15
+
16
+
17
+def _validate_subordinate_profile(agent: Agent, profile: str) -> str:
18
+ agent_profile = str(profile or "").strip()
19
+ if not agent_profile:
20
+ return ""
21
+
22
+ labels = _subordinate_profile_labels(agent)
23
+ if agent_profile in labels:
24
+ return agent_profile
25
+
26
+ available = ", ".join(
27
+ f"{key} ({label})" if label and label != key else key
28
+ for key, label in sorted(labels.items())
29
+ )
30
+ if not available:
31
+ available = "none"
32
+ raise RepairableException(
33
+ f"Agent profile '{agent_profile}' not found. Use one of the available profiles: {available}."
34
+ )
35
+
36
+
37
class Delegation(Tool):
38
39
async def execute(self, message="", reset="", **kwargs):
40
+ requested_profile = _validate_subordinate_profile(
41
+ self.agent, kwargs.get("profile", kwargs.get("agent_profile", ""))
42
+ )
43
+ existing_subordinate = self.agent.get_data(Agent.DATA_NAME_SUBORDINATE)
44
+ reset_requested = str(reset).lower().strip() == "true"
45
+
46
+ if existing_subordinate and requested_profile and not reset_requested:
47
+ current_profile = str(
48
+ getattr(getattr(existing_subordinate, "config", None), "profile", "")
49
+ or ""
50
+ )
51
+ if current_profile != requested_profile:
52
+ raise RepairableException(
53
+ f"Subordinate already uses profile '{current_profile or 'default'}'. "
54
+ f"Set reset=true to switch to '{requested_profile}'."
55
+ )
56
+
57
# create subordinate agent using the data object on this agent and set superior agent to his data object
58
if (
12
- self.agent.get_data(Agent.DATA_NAME_SUBORDINATE) is None
13
- or str(reset).lower().strip() == "true"
59
+ existing_subordinate is None
60
+ or reset_requested
61
):
15
- # initialize default config
16
- config = initialize_agent()
17
-
18
- # set subordinate prompt profile if provided, if not, keep original
19
- agent_profile = kwargs.get("profile", kwargs.get("agent_profile", ""))
20
- if agent_profile:
21
- config.profile = agent_profile
62
+ # set subordinate prompt profile if provided, otherwise use the default profile
63
+ override_settings = (
64
+ {"agent_profile": requested_profile} if requested_profile else None
65
+ )
66
+ config = initialize_agent(override_settings=override_settings)
67
23
- # crate agent
68
+ # create agent
69
sub = Agent(self.agent.number + 1, config, self.agent.context)
70
# register superior/subordinate
71
sub.set_data(Agent.DATA_NAME_SUPERIOR, self.agent)
tools/call_subordinate.py.dox.md
+8
-2
@@ -14,6 +14,9 @@
14
- `Delegation` (`Tool`)
15
- `async execute(self, message=..., reset=..., **kwargs)`
16
- `get_log_object(self)`
17
+- Top-level functions:
18
+- `_subordinate_profile_labels(agent: Agent) -> dict[str, str]`
19
+- `_validate_subordinate_profile(agent: Agent, profile: str) -> str`
20
21
## Runtime Contracts
22
@@ -22,11 +25,13 @@
25
- `Delegation` is a `Tool`.
26
- `Delegation` defines `execute(...)`.
27
- Observed side-effect areas: filesystem writes, settings/state persistence.
25
-- Imported dependency areas include: `agent`, `extensions.python.hist_add_tool_result`, `helpers.tool`, `initialize`.
28
+- `profile`/`agent_profile` values are validated against available profile keys before use; unknown profiles raise `RepairableException` so the agent can retry with a real profile.
29
+- Supplying a different profile for an existing subordinate without `reset=true` raises `RepairableException` instead of silently continuing the old subordinate.
30
+- Imported dependency areas include: `agent`, `extensions.python.hist_add_tool_result`, `helpers`, `helpers.errors`, `helpers.tool`.
31
32
## Key Concepts
33
29
-- Important called helpers/classes observed in the source: `self.agent.get_data`, `subordinate.hist_add_user_message`, `subordinate.history.new_topic`, `Response`, `self.agent.context.log.log`, `initialize_agent`, `Agent`, `sub.set_data`, `self.agent.set_data`, `UserMessage`, `subordinate.monologue`, `self.agent.read_prompt`, `str.lower.strip`, `str.lower`.
34
+- Important called helpers/classes observed in the source: `self.agent.get_data`, `projects.get_context_project_name`, `subagents.get_available_agents_dict`, `RepairableException`, `initialize_agent`, `subordinate.hist_add_user_message`, `subordinate.history.new_topic`, `Response`, `self.agent.context.log.log`, `Agent`, `sub.set_data`, `self.agent.set_data`, `UserMessage`, `subordinate.monologue`, `self.agent.read_prompt`, `str.lower.strip`, `str.lower`.
35
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
36
37
## Work Guidance
@@ -40,6 +45,7 @@
45
- Run targeted tool and prompt-contract tests for changed behavior; smoke-test agent execution when no focused test exists.
46
- Related tests observed by source search:
47
- `tests/test_default_prompt_budget.py`
48
+ - `tests/test_subagent_profiles.py`
49
50
## Child DOX Index
51