Preserve subordinate agent profiles
Some checks are pending
Build And Publish Docker Images / plan (push) Waiting to run
Build And Publish Docker Images / build (push) Blocked by required conditions

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.
This commit is contained in:
Alessandro 2026-06-17 13:07:22 +02:00
parent 630bfc16f2
commit b9153f718e
15 changed files with 353 additions and 47 deletions

View file

@ -1,4 +1,4 @@
from agent import Agent, AgentContext
from agent import AgentContext
from helpers import subagents
from helpers.api import ApiHandler, Request, Response
from helpers.persist_chat import save_tmp_chat
@ -39,11 +39,7 @@ class SetAgentProfile(ApiHandler):
config = initialize_agent(override_settings={"agent_profile": profile})
context.config = config
agent = context.agent0
while agent:
agent.config = config
agent = agent.get_data(Agent.DATA_NAME_SUBORDINATE)
context.agent0.config = config
save_tmp_chat(context)
mark_dirty_for_context(context.id, reason="agent_profile_change")

View file

@ -23,11 +23,12 @@
- `SetAgentProfile` is an `ApiHandler`.
- `SetAgentProfile` defines `process(...)`.
- Observed side-effect areas: filesystem writes, settings/state persistence.
- Imported dependency areas include: `agent`, `helpers`, `helpers.api`, `helpers.persist_chat`, `helpers.state_monitor_integration`, `initialize`.
- Switching a chat profile updates the context and top-level agent profile only; existing subordinate agents keep their own profile configs.
- Imported dependency areas include: `agent`, `helpers`, `helpers.api`, `helpers.persist_chat`, `helpers.state_monitor_integration`.
## Key Concepts
- 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`.
- 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`.
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
## Work Guidance
@ -39,7 +40,8 @@
## Verification
- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
- Related tests observed by source search:
- `tests/test_subagent_profiles.py`
## Child DOX Index

View file

@ -436,7 +436,6 @@ def _handle_model(context: "AgentContext", args: str) -> str:
def _handle_agent(context: "AgentContext", args: str) -> str:
from agent import Agent
from helpers import subagents
from initialize import initialize_agent
@ -468,10 +467,7 @@ def _handle_agent(context: "AgentContext", args: str) -> str:
config = initialize_agent(override_settings={"agent_profile": profile})
context.config = config
agent = context.agent0
while agent:
agent.config = config
agent = agent.get_data(Agent.DATA_NAME_SUBORDINATE)
context.agent0.config = config
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}."

View file

@ -31,10 +31,11 @@
- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
- Observed side-effect areas: filesystem writes, model calls, plugin state, settings/state persistence.
- Imported dependency areas include: `__future__`, `helpers`, `helpers.persist_chat`, `helpers.state_monitor_integration`, `plugins._model_config.helpers`, `re`, `typing`.
- `/agent` switches the top-level chat profile and preserves existing subordinate agent profiles.
## Key Concepts
- 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`.
- 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`.
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
## Work Guidance
@ -46,7 +47,8 @@
## Verification
- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
- Related tests observed by source search:
- `tests/test_subagent_profiles.py`
## Child DOX Index

View file

@ -132,8 +132,8 @@ def remove_msg_files(ctxid):
def _serialize_context(context: AgentContext):
profile = str(
getattr(context.config, "profile", None)
or getattr(context.agent0.config, "profile", None)
getattr(context.agent0.config, "profile", None)
or getattr(context.config, "profile", None)
or ""
)
@ -180,6 +180,7 @@ def _serialize_agent(agent: Agent):
return {
"number": agent.number,
"agent_profile": str(getattr(agent.config, "profile", "") or ""),
"data": data,
"history": history,
}
@ -232,11 +233,23 @@ def _deserialize_context(data):
streaming_agent = streaming_agent.data.get(Agent.DATA_NAME_SUBORDINATE, None)
context.agent0 = agent0
context.config = agent0.config
context.streaming_agent = streaming_agent
return context
def _deserialize_agent_config(
agent_data: dict[str, Any], fallback_config: AgentConfig
) -> AgentConfig:
fallback_profile = str(getattr(fallback_config, "profile", "") or "")
profile = str(agent_data.get("agent_profile") or fallback_profile).strip()
if profile == fallback_profile:
return fallback_config
override_settings = {"agent_profile": profile} if profile else None
return initialize_agent(override_settings=override_settings)
def _deserialize_agents(
agents: list[dict[str, Any]], config: AgentConfig, context: AgentContext
) -> Agent:
@ -246,7 +259,7 @@ def _deserialize_agents(
for ag in agents:
current = Agent(
number=ag["number"],
config=config,
config=_deserialize_agent_config(ag, config),
context=context,
)
current.data = ag.get("data", {})

View file

@ -28,6 +28,7 @@
- `_serialize_agent(agent: Agent)`
- `_serialize_log(log: Log)`
- `_deserialize_context(data)`
- `_deserialize_agent_config(agent_data: dict[str, Any], fallback_config: AgentConfig) -> AgentConfig`
- `_deserialize_agents(agents: list[dict[str, Any]], config: AgentConfig, context: AgentContext) -> Agent`
- `_deserialize_log(data: dict[str, Any]) -> 'Log'`
- `_safe_json_serialize(obj, **kwargs)`
@ -39,10 +40,12 @@
- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, settings/state persistence, scheduler state.
- Imported dependency areas include: `agent`, `collections`, `datetime`, `helpers`, `helpers.localization`, `helpers.log`, `initialize`, `json`, `typing`, `uuid`.
- 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.
- Deserialization must rebuild each agent with its serialized profile when present, falling back to the context profile for older chat files.
## Key Concepts
- 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`.
- 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`.
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
## Work Guidance
@ -58,6 +61,7 @@
- `tests/test_api_chat_lifetime.py`
- `tests/test_browser_agent_regressions.py`
- `tests/test_persist_chat_log_ids.py`
- `tests/test_subagent_profiles.py`
- `tests/test_tool_action_contracts.py`
## Child DOX Index

View file

@ -572,18 +572,27 @@ def _apply_settings(previous: Settings | None, browser_timezone: str | None = No
if _settings:
_apply_timezone_setting(previous, browser_timezone)
from agent import AgentContext
from agent import Agent, AgentContext
from initialize import initialize_agent
for ctx in AgentContext.all():
profile = str(getattr(ctx.config, "profile", "") or _settings["agent_profile"])
config = initialize_agent(override_settings={"agent_profile": profile})
ctx.config = config # reinitialize context config with new settings
# apply config to agents
profile = str(
getattr(ctx.config, "profile", "") or _settings["agent_profile"]
)
ctx.config = initialize_agent(override_settings={"agent_profile": profile})
agent = ctx.agent0
while agent:
agent.config = ctx.config
agent = agent.get_data(agent.DATA_NAME_SUBORDINATE)
agent_profile = str(
getattr(getattr(agent, "config", None), "profile", "") or profile
)
agent.config = (
ctx.config
if agent is ctx.agent0 and agent_profile == profile
else initialize_agent(
override_settings={"agent_profile": agent_profile}
)
)
agent = agent.get_data(Agent.DATA_NAME_SUBORDINATE)
# update mcp settings if necessary
if not previous or _settings["mcp_servers"] != previous["mcp_servers"]:

View file

@ -61,7 +61,8 @@
## Key Concepts
- 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`.
- 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`.
- Applying settings refreshes active context configs while preserving each subordinate agent's own profile.
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
## Work Guidance
@ -82,6 +83,7 @@
- `tests/test_model_config_api_keys.py`
- `tests/test_model_config_project_presets.py`
- `tests/test_oauth_static.py`
- `tests/test_subagent_profiles.py`
## Child DOX Index

View file

@ -433,4 +433,4 @@ def get_paths(
# end-of-file imports to prevent circular imports
from helpers import plugins
from helpers import plugins

View file

@ -16,6 +16,7 @@
- Treat bot tokens, chat IDs, attachments, and user data as sensitive.
- Keep allowed-user, group-mode, project, model, and `/send` controls enforced.
- Install Telegram dependencies into the framework runtime only when required.
- Agent profile picker actions change the top-level chat profile and must preserve existing subordinate agent profiles.
## Work Guidance

View file

@ -413,7 +413,6 @@ async def _select_project(context: AgentContext, index: int, *, clear: bool = Fa
async def _select_agent(context: AgentContext, index: int) -> None:
if context.is_running():
return
from agent import Agent
from initialize import initialize_agent
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:
profile = str(items[index]["key"])
config = initialize_agent(override_settings={"agent_profile": profile})
context.config = config
agent = context.agent0
while agent:
agent.config = config
agent = agent.get_data(Agent.DATA_NAME_SUBORDINATE)
context.agent0.config = config
save_tmp_chat(context)
mark_dirty_for_context(context.id, reason="telegram.agent_select")

View file

@ -1,7 +1,7 @@
### call_subordinate
delegate research or complex subtasks to a specialized agent.
args: `message`, optional `profile`, `reset`
- `profile`: optional prompt profile name for the subordinate; leave empty for the default profile
- `profile`: optional prompt profile key for the subordinate; when provided, it must exactly match an available profile; leave empty for the default profile
- `reset`: use json boolean `true` for the first message or when changing profile; use `false` to continue
- `message`: define role, goal, and the concrete task
after the subordinate returns, answer from its result directly when it satisfies the user request

View file

@ -0,0 +1,234 @@
from __future__ import annotations
from types import SimpleNamespace
import pytest
from agent import Agent, AgentConfig, AgentContext
from helpers import persist_chat
from helpers.errors import RepairableException
class _FakeContext:
id = "ctx"
def get_data(self, key: str, recursive: bool = True):
return None
class _FakeParentAgent:
def __init__(self) -> None:
self.number = 0
self.agent_name = "A0"
self.config = AgentConfig(mcp_servers="", profile="agent0")
self.context = _FakeContext()
self.data = {}
def get_data(self, key: str):
return self.data.get(key)
def set_data(self, key: str, value):
self.data[key] = value
def read_prompt(self, _file: str, **_kwargs) -> str:
return ""
class _FakeSubAgent:
DATA_NAME_SUPERIOR = "_superior"
DATA_NAME_SUBORDINATE = "_subordinate"
def __init__(self, number: int, config: AgentConfig, context) -> None:
self.number = number
self.config = config
self.context = context
self.data = {}
self.history = SimpleNamespace(new_topic=lambda: None)
self.messages = []
def set_data(self, key: str, value):
self.data[key] = value
def hist_add_user_message(self, message):
self.messages.append(message)
async def monologue(self):
return "delegated"
@pytest.mark.asyncio
async def test_call_subordinate_rejects_unknown_profile(monkeypatch) -> None:
import tools.call_subordinate as call_subordinate
monkeypatch.setattr(
call_subordinate,
"_subordinate_profile_labels",
lambda _agent: {"developer": "Developer", "researcher": "Researcher"},
)
parent = _FakeParentAgent()
tool = call_subordinate.Delegation(
parent, # type: ignore[arg-type]
"call_subordinate",
None,
{"profile": "ghost", "message": "work"},
"",
None,
)
with pytest.raises(RepairableException, match="Agent profile 'ghost' not found"):
await tool.execute(message="work", profile="ghost", reset=True)
assert parent.data == {}
@pytest.mark.asyncio
async def test_call_subordinate_uses_valid_profile(monkeypatch) -> None:
import tools.call_subordinate as call_subordinate
monkeypatch.setattr(call_subordinate, "Agent", _FakeSubAgent)
monkeypatch.setattr(
call_subordinate,
"_subordinate_profile_labels",
lambda _agent: {"developer": "Developer"},
)
monkeypatch.setattr(
call_subordinate,
"initialize_agent",
lambda override_settings=None: AgentConfig(
mcp_servers="",
profile=(override_settings or {}).get("agent_profile", "agent0"),
),
)
parent = _FakeParentAgent()
tool = call_subordinate.Delegation(
parent, # type: ignore[arg-type]
"call_subordinate",
None,
{"profile": "developer", "message": "work"},
"",
None,
)
response = await tool.execute(message="work", profile="developer", reset=True)
child = parent.get_data(_FakeSubAgent.DATA_NAME_SUBORDINATE)
assert response.message == "delegated"
assert child.config.profile == "developer"
assert child.messages[0].message == "work"
@pytest.mark.asyncio
async def test_call_subordinate_requires_reset_to_change_existing_profile(monkeypatch) -> None:
import tools.call_subordinate as call_subordinate
monkeypatch.setattr(
call_subordinate,
"_subordinate_profile_labels",
lambda _agent: {"developer": "Developer", "researcher": "Researcher"},
)
parent = _FakeParentAgent()
existing = SimpleNamespace(config=AgentConfig(mcp_servers="", profile="developer"))
parent.set_data(_FakeSubAgent.DATA_NAME_SUBORDINATE, existing)
tool = call_subordinate.Delegation(
parent, # type: ignore[arg-type]
"call_subordinate",
None,
{"profile": "researcher", "message": "work"},
"",
None,
)
with pytest.raises(RepairableException, match="Set reset=true"):
await tool.execute(message="work", profile="researcher", reset=False)
def test_persist_chat_roundtrip_preserves_each_agent_profile(monkeypatch) -> None:
monkeypatch.setattr(
persist_chat,
"initialize_agent",
lambda override_settings=None: AgentConfig(
mcp_servers="",
profile=(override_settings or {}).get("agent_profile", "agent0"),
),
)
context_id = "ctx-subagent-profile"
AgentContext.remove(context_id)
context = AgentContext(
config=AgentConfig(mcp_servers="", profile="agent0"),
id=context_id,
set_current=False,
)
child = Agent(1, AgentConfig(mcp_servers="", profile="developer"), context)
context.agent0.set_data(Agent.DATA_NAME_SUBORDINATE, child)
child.set_data(Agent.DATA_NAME_SUPERIOR, context.agent0)
try:
serialized = persist_chat._serialize_context(context)
assert serialized["agent_profile"] == "agent0"
assert serialized["agents"][0]["agent_profile"] == "agent0"
assert serialized["agents"][1]["agent_profile"] == "developer"
AgentContext.remove(context_id)
restored = persist_chat._deserialize_context(serialized)
restored_child = restored.agent0.get_data(Agent.DATA_NAME_SUBORDINATE)
assert restored.config.profile == "agent0"
assert restored.agent0.config.profile == "agent0"
assert restored_child.config.profile == "developer"
finally:
AgentContext.remove(context_id)
@pytest.mark.asyncio
async def test_agent_profile_set_preserves_subagent_profile(monkeypatch) -> None:
import api.agent_profile_set as agent_profile_set
monkeypatch.setattr(
agent_profile_set,
"_agent_profile_labels",
lambda: {"researcher": "Researcher"},
)
monkeypatch.setattr(
agent_profile_set,
"initialize_agent",
lambda override_settings=None: AgentConfig(
mcp_servers="",
profile=(override_settings or {}).get("agent_profile", "agent0"),
),
)
monkeypatch.setattr(agent_profile_set, "save_tmp_chat", lambda _context: None)
monkeypatch.setattr(
agent_profile_set,
"mark_dirty_for_context",
lambda *_args, **_kwargs: None,
)
context_id = "ctx-profile-switch"
AgentContext.remove(context_id)
context = AgentContext(
config=AgentConfig(mcp_servers="", profile="agent0"),
id=context_id,
set_current=False,
)
child = Agent(1, AgentConfig(mcp_servers="", profile="developer"), context)
context.agent0.set_data(Agent.DATA_NAME_SUBORDINATE, child)
child.set_data(Agent.DATA_NAME_SUPERIOR, context.agent0)
try:
handler = agent_profile_set.SetAgentProfile.__new__(
agent_profile_set.SetAgentProfile
)
response = await handler.process(
{"context_id": context_id, "agent_profile": "researcher"},
request=None, # type: ignore[arg-type]
)
assert response["ok"] is True
assert context.config.profile == "researcher"
assert context.agent0.config.profile == "researcher"
assert child.config.profile == "developer"
finally:
AgentContext.remove(context_id)

View file

@ -1,26 +1,71 @@
from agent import Agent, UserMessage
from helpers import projects, subagents
from helpers.errors import RepairableException
from helpers.tool import Tool, Response
from initialize import initialize_agent
from extensions.python.hist_add_tool_result import _90_save_tool_call_file as save_tool_call_file
def _subordinate_profile_labels(agent: Agent) -> dict[str, str]:
project = projects.get_context_project_name(agent.context) if agent.context else None
return {
name: subagent.title or name
for name, subagent in subagents.get_available_agents_dict(project).items()
}
def _validate_subordinate_profile(agent: Agent, profile: str) -> str:
agent_profile = str(profile or "").strip()
if not agent_profile:
return ""
labels = _subordinate_profile_labels(agent)
if agent_profile in labels:
return agent_profile
available = ", ".join(
f"{key} ({label})" if label and label != key else key
for key, label in sorted(labels.items())
)
if not available:
available = "none"
raise RepairableException(
f"Agent profile '{agent_profile}' not found. Use one of the available profiles: {available}."
)
class Delegation(Tool):
async def execute(self, message="", reset="", **kwargs):
requested_profile = _validate_subordinate_profile(
self.agent, kwargs.get("profile", kwargs.get("agent_profile", ""))
)
existing_subordinate = self.agent.get_data(Agent.DATA_NAME_SUBORDINATE)
reset_requested = str(reset).lower().strip() == "true"
if existing_subordinate and requested_profile and not reset_requested:
current_profile = str(
getattr(getattr(existing_subordinate, "config", None), "profile", "")
or ""
)
if current_profile != requested_profile:
raise RepairableException(
f"Subordinate already uses profile '{current_profile or 'default'}'. "
f"Set reset=true to switch to '{requested_profile}'."
)
# create subordinate agent using the data object on this agent and set superior agent to his data object
if (
self.agent.get_data(Agent.DATA_NAME_SUBORDINATE) is None
or str(reset).lower().strip() == "true"
existing_subordinate is None
or reset_requested
):
# initialize default config
config = initialize_agent()
# set subordinate prompt profile if provided, otherwise use the default profile
override_settings = (
{"agent_profile": requested_profile} if requested_profile else None
)
config = initialize_agent(override_settings=override_settings)
# set subordinate prompt profile if provided, if not, keep original
agent_profile = kwargs.get("profile", kwargs.get("agent_profile", ""))
if agent_profile:
config.profile = agent_profile
# crate agent
# create agent
sub = Agent(self.agent.number + 1, config, self.agent.context)
# register superior/subordinate
sub.set_data(Agent.DATA_NAME_SUPERIOR, self.agent)

View file

@ -14,6 +14,9 @@
- `Delegation` (`Tool`)
- `async execute(self, message=..., reset=..., **kwargs)`
- `get_log_object(self)`
- Top-level functions:
- `_subordinate_profile_labels(agent: Agent) -> dict[str, str]`
- `_validate_subordinate_profile(agent: Agent, profile: str) -> str`
## Runtime Contracts
@ -22,11 +25,13 @@
- `Delegation` is a `Tool`.
- `Delegation` defines `execute(...)`.
- Observed side-effect areas: filesystem writes, settings/state persistence.
- Imported dependency areas include: `agent`, `extensions.python.hist_add_tool_result`, `helpers.tool`, `initialize`.
- `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.
- Supplying a different profile for an existing subordinate without `reset=true` raises `RepairableException` instead of silently continuing the old subordinate.
- Imported dependency areas include: `agent`, `extensions.python.hist_add_tool_result`, `helpers`, `helpers.errors`, `helpers.tool`.
## Key Concepts
- 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`.
- 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`.
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
## Work Guidance
@ -40,6 +45,7 @@
- Run targeted tool and prompt-contract tests for changed behavior; smoke-test agent execution when no focused test exists.
- Related tests observed by source search:
- `tests/test_default_prompt_budget.py`
- `tests/test_subagent_profiles.py`
## Child DOX Index