Skip to repository content312 lines · 11.5 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:35:24.313Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
spawn_agent_tool.rs
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