Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:35:24.313Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

spawn_agent_tool.rs

312 lines · 11.5 KB · rust
1use acp_thread::{SUBAGENT_SESSION_INFO_META_KEY, SubagentSessionInfo};
2use agent_client_protocol::schema::v1 as acp;
3use anyhow::Result;
4use gpui::{App, SharedString, Task};
5use language_model::LanguageModelToolResultContent;
6use schemars::JsonSchema;
7use serde::{Deserialize, Deserializer, Serialize};
8use std::rc::Rc;
9use std::sync::Arc;
10
11use crate::{AgentTool, ThreadEnvironment, ToolCallEventStream, ToolInput};
12
13/// Spawn a sub-agent for a well-scoped task.
14///
15/// ### Designing delegated subtasks
16/// - An agent does not see your conversation history. Include all relevant context (file paths, requirements, constraints) in the message.
17/// - Subtasks must be concrete, well-defined, and self-contained.
18/// - Delegated subtasks must materially advance the main task.
19/// - Do not duplicate work between your work and delegated subtasks.
20/// - Do not use this tool for tasks you could accomplish directly with one or two tool calls. For example, don't ask the agent to read a single file and return the contents, you can do this yourself.
21/// - When you delegate work, focus on coordinating and synthesizing results instead of duplicating the same work yourself.
22/// - Avoid issuing multiple delegate calls for the same unresolved subproblem unless the new delegated task is genuinely different and necessary.
23/// - Narrow the delegated ask to the concrete output you need next.
24/// - For code-edit subtasks, decompose work so each delegated task has a disjoint write set.
25/// - When sending a follow-up using an existing agent session_id, the agent already has the context from the previous turn. Send only a short, direct message. Do NOT repeat the original task or context.
26///
27/// ### Parallel delegation patterns
28/// - Run multiple independent information-seeking subtasks in parallel when you have distinct questions that can be answered independently.
29/// - Split implementation into disjoint codebase slices and spawn multiple agents for them in parallel when the write scopes do not overlap.
30/// - When a plan has multiple independent steps, prefer delegating those steps in parallel rather than serializing them unnecessarily.
31/// - Reuse the returned session_id when you want to follow up on the same delegated subproblem instead of creating a duplicate session.
32///
33/// ### Output
34/// - You will receive only the agent's final message as output.
35/// - Successful calls return a session_id that you can use for follow-up messages.
36/// - Error results may also include a session_id if a session was already created.
37#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
38#[serde(rename_all = "snake_case")]
39pub struct SpawnAgentToolInput {
40    /// Short label displayed in the UI while the agent runs (e.g., "Researching alternatives")
41    pub label: String,
42    /// The prompt for the agent. For new sessions, include full context needed for the task. For follow-ups (with session_id), you can rely on the agent already having the previous message.
43    pub message: String,
44    /// Session ID of an existing agent session to continue instead of creating a new one. Omit to create a new agent.
45    #[serde(default, deserialize_with = "deserialize_session_id")]
46    pub session_id: Option<acp::SessionId>,
47}
48
49fn deserialize_session_id<'de, D>(deserializer: D) -> Result<Option<acp::SessionId>, D::Error>
50where
51    D: Deserializer<'de>,
52{
53    let Some(value) = Option::<serde_json::Value>::deserialize(deserializer)? else {
54        return Ok(None);
55    };
56
57    if value
58        .as_str()
59        .is_some_and(|session_id| session_id.trim().is_empty())
60    {
61        return Ok(None);
62    }
63
64    serde_json::from_value(value)
65        .map(Some)
66        .map_err(serde::de::Error::custom)
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70#[serde(untagged)]
71#[serde(rename_all = "snake_case")]
72pub enum SpawnAgentToolOutput {
73    Success {
74        session_id: acp::SessionId,
75        output: String,
76        session_info: SubagentSessionInfo,
77    },
78    Error {
79        #[serde(skip_serializing_if = "Option::is_none")]
80        #[serde(default)]
81        session_id: Option<acp::SessionId>,
82        error: String,
83        session_info: Option<SubagentSessionInfo>,
84    },
85}
86
87impl From<SpawnAgentToolOutput> for LanguageModelToolResultContent {
88    fn from(output: SpawnAgentToolOutput) -> Self {
89        match output {
90            SpawnAgentToolOutput::Success {
91                session_id,
92                output,
93                session_info: _, // Don't show this to the model
94            } => serde_json::to_string(
95                &serde_json::json!({ "session_id": session_id, "output": output }),
96            )
97            .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}"))
98            .into(),
99            SpawnAgentToolOutput::Error {
100                session_id,
101                error,
102                session_info: _, // Don't show this to the model
103            } => serde_json::to_string(
104                &serde_json::json!({ "session_id": session_id, "error": error }),
105            )
106            .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}"))
107            .into(),
108        }
109    }
110}
111
112/// Tool that spawns an agent thread to work on a task.
113pub struct SpawnAgentTool {
114    environment: Rc<dyn ThreadEnvironment>,
115}
116
117impl SpawnAgentTool {
118    pub fn new(environment: Rc<dyn ThreadEnvironment>) -> Self {
119        Self { environment }
120    }
121}
122
123impl AgentTool for SpawnAgentTool {
124    type Input = SpawnAgentToolInput;
125    type Output = SpawnAgentToolOutput;
126
127    const NAME: &'static str = "spawn_agent";
128
129    fn kind() -> acp::ToolKind {
130        acp::ToolKind::Other
131    }
132
133    fn initial_title(
134        &self,
135        input: Result<Self::Input, serde_json::Value>,
136        _cx: &mut App,
137    ) -> SharedString {
138        match input {
139            Ok(i) => i.label.into(),
140            Err(value) => value
141                .get("label")
142                .and_then(|v| v.as_str())
143                .map(|s| SharedString::from(s.to_owned()))
144                .unwrap_or_else(|| "Spawning agent".into()),
145        }
146    }
147
148    fn run(
149        self: Arc<Self>,
150        input: ToolInput<Self::Input>,
151        event_stream: ToolCallEventStream,
152        cx: &mut App,
153    ) -> Task<Result<Self::Output, Self::Output>> {
154        cx.spawn(async move |cx| {
155            let input = input
156                .recv()
157                .await
158                .map_err(|e| SpawnAgentToolOutput::Error {
159                    session_id: None,
160                    error: e.to_string(),
161                    session_info: None,
162                })?;
163
164            let (subagent, mut session_info) = cx.update(|cx| {
165                let subagent = if let Some(session_id) = input.session_id {
166                    self.environment.resume_subagent(session_id, cx)
167                } else {
168                    self.environment.create_subagent(input.label, cx)
169                };
170                let subagent = subagent.map_err(|err| SpawnAgentToolOutput::Error {
171                    session_id: None,
172                    error: err.to_string(),
173                    session_info: None,
174                })?;
175                let session_info = SubagentSessionInfo {
176                    session_id: subagent.id(),
177                    message_start_index: subagent.num_entries(cx),
178                    message_end_index: None,
179                };
180
181                event_stream.subagent_spawned(subagent.id());
182                event_stream.update_fields_with_meta(
183                    acp::ToolCallUpdateFields::new(),
184                    Some(acp::Meta::from_iter([(
185                        SUBAGENT_SESSION_INFO_META_KEY.into(),
186                        serde_json::json!(&session_info),
187                    )])),
188                );
189
190                Ok((subagent, session_info))
191            })?;
192
193            let send_result = subagent.send(input.message, cx).await;
194
195            let status = if send_result.is_ok() {
196                "completed"
197            } else {
198                "error"
199            };
200            telemetry::event!(
201                "Subagent Completed",
202                subagent_session = session_info.session_id.to_string(),
203                status,
204            );
205
206            session_info.message_end_index =
207                cx.update(|cx| Some(subagent.num_entries(cx).saturating_sub(1)));
208
209            let meta = Some(acp::Meta::from_iter([(
210                SUBAGENT_SESSION_INFO_META_KEY.into(),
211                serde_json::json!(&session_info),
212            )]));
213
214            let (output, result) = match send_result {
215                Ok(output) => (
216                    output.clone(),
217                    Ok(SpawnAgentToolOutput::Success {
218                        session_id: session_info.session_id.clone(),
219                        session_info,
220                        output,
221                    }),
222                ),
223                Err(e) => {
224                    let error = e.to_string();
225                    (
226                        error.clone(),
227                        Err(SpawnAgentToolOutput::Error {
228                            session_id: Some(session_info.session_id.clone()),
229                            error,
230                            session_info: Some(session_info),
231                        }),
232                    )
233                }
234            };
235            event_stream.update_fields_with_meta(
236                acp::ToolCallUpdateFields::new().content(vec![output.into()]),
237                meta,
238            );
239            result
240        })
241    }
242
243    fn replay(
244        &self,
245        _input: Self::Input,
246        output: Self::Output,
247        event_stream: ToolCallEventStream,
248        _cx: &mut App,
249    ) -> Result<()> {
250        let (content, session_info) = match output {
251            SpawnAgentToolOutput::Success {
252                output,
253                session_info,
254                ..
255            } => (output.into(), Some(session_info)),
256            SpawnAgentToolOutput::Error {
257                error,
258                session_info,
259                ..
260            } => (error.into(), session_info),
261        };
262
263        let meta = session_info.map(|session_info| {
264            acp::Meta::from_iter([(
265                SUBAGENT_SESSION_INFO_META_KEY.into(),
266                serde_json::json!(&session_info),
267            )])
268        });
269        event_stream.update_fields_with_meta(
270            acp::ToolCallUpdateFields::new().content(vec![content]),
271            meta,
272        );
273
274        Ok(())
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281    use serde_json::json;
282
283    #[test]
284    fn deserializes_blank_session_id_as_absent() {
285        for session_id in [json!(null), json!(""), json!("   ")] {
286            let input: SpawnAgentToolInput = serde_json::from_value(json!({
287                "label": "label",
288                "message": "message",
289                "session_id": session_id,
290            }))
291            .unwrap();
292
293            assert!(input.session_id.is_none());
294        }
295
296        let input: SpawnAgentToolInput = serde_json::from_value(json!({
297            "label": "label",
298            "message": "message",
299        }))
300        .unwrap();
301        assert!(input.session_id.is_none());
302
303        let input: SpawnAgentToolInput = serde_json::from_value(json!({
304            "label": "label",
305            "message": "message",
306            "session_id": "existing-session",
307        }))
308        .unwrap();
309        assert_eq!(input.session_id.unwrap().to_string(), "existing-session");
310    }
311}
312
Served at tenant.openagents/omega Member data and write actions are omitted.