File size: 9,476 Bytes
6918275
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
from agent import Agent, AgentContext, UserMessage
from helpers import message_queue, persist_chat, 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


SUBORDINATES_DATA_KEY = "_subordinates"
CHILD_PARENT_CONTEXT_ID_KEY = "parent_context_id"
CHILD_PARENT_AGENT_NUMBER_KEY = "parent_agent_number"
CHILD_PARENT_CONTEXT_KIND_KEY = "parent_context_kind"
CHILD_PARENT_CONTEXT_LABEL_KEY = "parent_context_label"
CHILD_SUBORDINATE_SLOT_KEY = "subordinate_slot"
DEFAULT_SUBORDINATE_SLOT = "default"


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}."
    )


def _register_subordinate(parent: Agent, subordinate: Agent, slot: str) -> None:
    subordinates = parent.get_data(SUBORDINATES_DATA_KEY)
    if not isinstance(subordinates, dict):
        subordinates = {}
        parent.set_data(SUBORDINATES_DATA_KEY, subordinates)
    subordinates[subordinate.context.id] = subordinate
    subordinate.set_data(Agent.DATA_NAME_SUPERIOR, parent)
    if slot == DEFAULT_SUBORDINATE_SLOT and subordinate.context is parent.context:
        parent.set_data(Agent.DATA_NAME_SUBORDINATE, subordinate)


def _is_child_context(context: AgentContext, parent: Agent, slot: str | None = None) -> bool:
    if context.get_output_data(CHILD_PARENT_CONTEXT_ID_KEY) != parent.context.id:
        return False
    if context.get_output_data(CHILD_PARENT_AGENT_NUMBER_KEY) != parent.number:
        return False
    if context.agent0.number != parent.number + 1:
        return False
    return slot is None or context.get_output_data(CHILD_SUBORDINATE_SLOT_KEY) == slot


def _is_live_context(context: AgentContext) -> bool:
    return not isinstance(context, AgentContext) or AgentContext.get(context.id) is context


def _find_subordinate(parent: Agent, context_id: str, slot: str) -> Agent | None:
    registered = parent.get_data(SUBORDINATES_DATA_KEY)
    registered = registered if isinstance(registered, dict) else {}
    if context_id:
        subordinate = registered.get(context_id)
        if (
            subordinate
            and _is_live_context(subordinate.context)
            and _is_child_context(subordinate.context, parent)
        ):
            return subordinate
        context = AgentContext.get(context_id)
        if not context or not _is_child_context(context, parent):
            raise RepairableException(
                f"Subordinate context '{context_id}' was not found under {parent.agent_name}."
            )
        subordinate = context.agent0
        _register_subordinate(parent, subordinate, slot)
        return subordinate

    existing = parent.get_data(Agent.DATA_NAME_SUBORDINATE)
    if slot == DEFAULT_SUBORDINATE_SLOT and existing is not None:
        return existing

    registered_matches = [
        subordinate
        for subordinate in registered.values()
        if _is_live_context(subordinate.context)
        and _is_child_context(subordinate.context, parent, slot)
    ]
    if registered_matches:
        return max(
            registered_matches,
            key=lambda subordinate: subordinate.context.created_at,
        )

    matches = [
        context
        for context in AgentContext.all()
        if _is_child_context(context, parent, slot)
    ]
    if not matches:
        return None
    subordinate = max(matches, key=lambda context: context.created_at).agent0
    _register_subordinate(parent, subordinate, slot)
    return subordinate


def get_or_create_subordinate(
    parent: Agent,
    *,
    profile: str = "",
    reset: bool | str = False,
    context_id: str = "",
    name: str = "",
    message: str = "",
    slot: str = DEFAULT_SUBORDINATE_SLOT,
) -> Agent:
    requested_profile = _validate_subordinate_profile(parent, profile)
    target_context_id = str(context_id or "").strip()
    reset_requested = str(reset).lower().strip() == "true"
    if target_context_id and reset_requested:
        raise RepairableException(
            "`context_id` continues an existing subordinate and requires reset=false. "
            "Omit `context_id` to create a fresh subordinate."
        )

    subordinate = (
        None
        if reset_requested
        else _find_subordinate(parent, target_context_id, slot)
    )
    if subordinate:
        current_profile = str(getattr(subordinate.config, "profile", "") or "")
        if requested_profile and current_profile != requested_profile:
            raise RepairableException(
                f"Subordinate already uses profile '{current_profile or 'default'}'. "
                f"Set reset=true and omit `context_id` to switch to '{requested_profile}'."
            )
        if subordinate.context is not parent.context and subordinate.context.is_running():
            raise RepairableException(
                f"Subordinate context '{subordinate.context.id}' is still running. "
                "Await or cancel its parallel job before continuing it."
            )
        return subordinate

    override_settings = {"agent_profile": requested_profile} if requested_profile else None
    subordinate = Agent(parent.number + 1, initialize_agent(override_settings=override_settings))
    context = subordinate.context
    context.name = str(name or "").strip() or _short_label(message) or subordinate.agent_name
    context.set_output_data(CHILD_PARENT_CONTEXT_ID_KEY, parent.context.id)
    context.set_output_data(CHILD_PARENT_AGENT_NUMBER_KEY, parent.number)
    context.set_output_data(CHILD_PARENT_CONTEXT_KIND_KEY, "subordinate")
    context.set_output_data(CHILD_PARENT_CONTEXT_LABEL_KEY, context.name)
    context.set_output_data(CHILD_SUBORDINATE_SLOT_KEY, slot)

    project = projects.get_context_project_name(parent.context)
    if project:
        projects.activate_project(context.id, project, mark_dirty=False)
    model_override = parent.context.get_data("chat_model_override")
    if model_override:
        context.set_data("chat_model_override", model_override)

    _register_subordinate(parent, subordinate, slot)
    return subordinate


async def run_subordinate(
    parent: Agent,
    subordinate: Agent,
    message: str,
    attachments: list[str] | None = None,
) -> str:
    assignment = str(message or "").strip()
    if not assignment:
        raise RepairableException("call_subordinate requires a non-empty `message`.")

    attachment_paths = [str(item) for item in attachments or []]
    if subordinate.context is not parent.context:
        message_queue.log_user_message(
            subordinate.context,
            assignment,
            attachment_paths,
            source=" (subordinate)",
        )
    subordinate.hist_add_user_message(
        UserMessage(message=assignment, attachments=attachment_paths)
    )
    if subordinate.context is not parent.context:
        persist_chat.save_tmp_chat(subordinate.context)

    try:
        result = await subordinate.monologue()
        subordinate.history.new_topic()
        return result
    finally:
        if subordinate.context is not parent.context:
            persist_chat.save_tmp_chat(subordinate.context)


def _short_label(text: str, limit: int = 80) -> str:
    return " ".join(str(text or "").split())[:limit].rstrip()


class Delegation(Tool):

    async def execute(self, message="", reset="", context_id="", **kwargs):
        attachments = kwargs.get("attachments")
        attachments = attachments if isinstance(attachments, list) else []
        subordinate = get_or_create_subordinate(
            self.agent,
            profile=kwargs.get("profile", kwargs.get("agent_profile", "")),
            reset=reset,
            context_id=context_id or kwargs.get("agent_id", ""),
            name=kwargs.get("name", ""),
            message=message,
        )
        result = await run_subordinate(self.agent, subordinate, message, attachments)

        # hint to use includes for long responses
        additional = {"context_id": subordinate.context.id}
        if len(result) >= save_tool_call_file.LEN_MIN:
            hint = self.agent.read_prompt("fw.hint.call_sub.md")
            if hint:
                additional["hint"] = hint

        # result
        return Response(message=result, break_loop=False, additional=additional)

    def get_log_object(self):
        return self.agent.context.log.log(
            type="subagent",
            heading=f"icon://communication {self.agent.agent_name}: Calling Subordinate Agent",
            content="",
            kvps=self.args,
        )