summaryrefslogtreecommitdiff
path: root/Omni/Agent
diff options
context:
space:
mode:
authorBen Sima <ben@bensima.com>2025-12-25 16:34:17 -0500
committerBen Sima <ben@bensima.com>2025-12-25 16:34:17 -0500
commit7c9e32f4dee52433911b7e82a72566dc8b5b5708 (patch)
treeff6d1cdb87b2383b334d79864d0c9b6e1a7062ff /Omni/Agent
parent66d2298f29f8e054687acc9e9615ddfa3cdb604a (diff)
Omni/Agent/Subagent: add General role and per-spawn customization
- Add SpawnGuardrails type for per-spawn resource limits - Extend SubagentConfig with toolsOverride, systemPrompt, guardrails - Add General role with balanced tools (file ops, bash, python, search) - Update spawn_subagent schema to expose general/custom roles and new params - Add subagent delegation guidance to Ava's system prompt 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'Omni/Agent')
-rw-r--r--Omni/Agent/Subagent.hs134
-rw-r--r--Omni/Agent/Telegram.hs19
2 files changed, 146 insertions, 7 deletions
diff --git a/Omni/Agent/Subagent.hs b/Omni/Agent/Subagent.hs
index 29286a0..597b361 100644
--- a/Omni/Agent/Subagent.hs
+++ b/Omni/Agent/Subagent.hs
@@ -100,6 +100,7 @@ import qualified Omni.Agent.Prompts as Prompts
import qualified Omni.Agent.Provider as Provider
import qualified Omni.Agent.Subagent.Coder as Coder
import qualified Omni.Agent.Tools as Tools
+import qualified Omni.Agent.Tools.Python as Python
import qualified Omni.Agent.Tools.WebReader as WebReader
import qualified Omni.Agent.Tools.WebSearch as WebSearch
import qualified Omni.Test as Test
@@ -306,6 +307,7 @@ data SubagentRole
| DataExtractor
| Researcher
| Coder
+ | General
| CustomRole Text
deriving (Show, Eq, Generic)
@@ -315,6 +317,7 @@ instance Aeson.ToJSON SubagentRole where
toJSON DataExtractor = Aeson.String "data_extractor"
toJSON Researcher = Aeson.String "researcher"
toJSON Coder = Aeson.String "coder"
+ toJSON General = Aeson.String "general"
toJSON (CustomRole name) = Aeson.String name
instance Aeson.FromJSON SubagentRole where
@@ -325,8 +328,36 @@ instance Aeson.FromJSON SubagentRole where
parseRole "data_extractor" = pure DataExtractor
parseRole "researcher" = pure Researcher
parseRole "coder" = pure Coder
+ parseRole "general" = pure General
parseRole name = pure (CustomRole name)
+-- | Per-spawn guardrails that override engine defaults
+data SpawnGuardrails = SpawnGuardrails
+ { spawnMaxCostCents :: Maybe Double,
+ spawnMaxTokens :: Maybe Int,
+ spawnMaxIterations :: Maybe Int,
+ spawnMaxDuplicateToolCalls :: Maybe Int
+ }
+ deriving (Show, Eq, Generic)
+
+instance Aeson.ToJSON SpawnGuardrails where
+ toJSON g =
+ Aeson.object
+ <| catMaybes
+ [ ("max_cost_cents" .=) </ spawnMaxCostCents g,
+ ("max_tokens" .=) </ spawnMaxTokens g,
+ ("max_iterations" .=) </ spawnMaxIterations g,
+ ("max_duplicate_tool_calls" .=) </ spawnMaxDuplicateToolCalls g
+ ]
+
+instance Aeson.FromJSON SpawnGuardrails where
+ parseJSON =
+ Aeson.withObject "SpawnGuardrails" <| \v ->
+ (SpawnGuardrails </ (v .:? "max_cost_cents"))
+ <*> (v .:? "max_tokens")
+ <*> (v .:? "max_iterations")
+ <*> (v .:? "max_duplicate_tool_calls")
+
data SubagentConfig = SubagentConfig
{ subagentRole :: SubagentRole,
subagentTask :: Text,
@@ -340,7 +371,13 @@ data SubagentConfig = SubagentConfig
-- | Optional task ID for tracking (not used by Coder)
subagentTaskId :: Maybe Text,
-- | Namespace for Coder role - required (e.g., "Omni/Agent/Subagent")
- subagentNamespace :: Maybe Text
+ subagentNamespace :: Maybe Text,
+ -- | Override tools for this spawn (Nothing = use role defaults)
+ subagentToolsOverride :: Maybe [Text],
+ -- | Additional system prompt instructions prepended to role prompt
+ subagentSystemPrompt :: Maybe Text,
+ -- | Per-spawn guardrails that override defaults
+ subagentGuardrails :: Maybe SpawnGuardrails
}
deriving (Show, Eq, Generic)
@@ -358,7 +395,10 @@ instance Aeson.ToJSON SubagentConfig where
Just ("extended_thinking" .= subagentExtendedThinking c),
("context" .=) </ subagentContext c,
("task_id" .=) </ subagentTaskId c,
- ("namespace" .=) </ subagentNamespace c
+ ("namespace" .=) </ subagentNamespace c,
+ ("tools" .=) </ subagentToolsOverride c,
+ ("system_prompt" .=) </ subagentSystemPrompt c,
+ ("guardrails" .=) </ subagentGuardrails c
]
instance Aeson.FromJSON SubagentConfig where
@@ -375,6 +415,9 @@ instance Aeson.FromJSON SubagentConfig where
<*> (v .:? "context")
<*> (v .:? "task_id")
<*> (v .:? "namespace")
+ <*> (v .:? "tools")
+ <*> (v .:? "system_prompt")
+ <*> (v .:? "guardrails")
data SubagentResult = SubagentResult
{ subagentOutput :: Aeson.Value,
@@ -639,7 +682,10 @@ defaultSubagentConfig role task =
subagentExtendedThinking = False,
subagentContext = Nothing,
subagentTaskId = Nothing,
- subagentNamespace = Nothing
+ subagentNamespace = Nothing,
+ subagentToolsOverride = Nothing,
+ subagentSystemPrompt = Nothing,
+ subagentGuardrails = Nothing
}
modelForRole :: SubagentRole -> Text
@@ -648,6 +694,7 @@ modelForRole CodeReviewer = "anthropic/claude-sonnet-4"
modelForRole DataExtractor = "anthropic/claude-3-haiku"
modelForRole Researcher = "anthropic/claude-sonnet-4"
modelForRole Coder = "anthropic/claude-sonnet-4"
+modelForRole General = "anthropic/claude-sonnet-4"
modelForRole (CustomRole _) = "anthropic/claude-sonnet-4"
data SubagentApiKeys = SubagentApiKeys
@@ -688,6 +735,16 @@ toolsForRole Researcher keys =
]
-- Coder uses the hardened Coder module, toolsForRole not used
toolsForRole Coder _keys = Coder.coderTools
+-- General role: balanced tools for non-specialized tasks
+toolsForRole General _keys =
+ [ Tools.readFileTool,
+ Tools.writeFileTool,
+ Tools.editFileTool,
+ Tools.runBashTool,
+ Python.pythonExecTool,
+ Tools.searchCodebaseTool,
+ Tools.searchAndReadTool
+ ]
toolsForRole (CustomRole _) keys = toolsForRole Researcher keys
-- | Load system prompt from template, falling back to hardcoded if unavailable
@@ -748,6 +805,7 @@ roleDescription CodeReviewer = "code review"
roleDescription DataExtractor = "data extraction"
roleDescription Researcher = "research"
roleDescription Coder = "coding"
+roleDescription General = "general-purpose"
roleDescription (CustomRole name) = name
runSubagent :: SubagentApiKeys -> SubagentConfig -> IO SubagentResult
@@ -977,7 +1035,9 @@ spawnSubagentTool keys =
<> "after the user explicitly approves. "
<> "Available roles: web_crawler (fast web research), code_reviewer (thorough code analysis), "
<> "data_extractor (structured data extraction), researcher (general research), "
- <> "coder (hardened coding with init/verify/commit - requires namespace and context).",
+ <> "coder (hardened coding with init/verify/commit - requires namespace and context), "
+ <> "general (balanced tools for non-specialized tasks), "
+ <> "custom (use custom_role_name and specify tools).",
Engine.toolJsonSchema =
Aeson.object
[ "type" .= ("object" :: Text),
@@ -986,7 +1046,7 @@ spawnSubagentTool keys =
[ "role"
.= Aeson.object
[ "type" .= ("string" :: Text),
- "enum" .= (["web_crawler", "code_reviewer", "data_extractor", "researcher", "coder"] :: [Text]),
+ "enum" .= (["web_crawler", "code_reviewer", "data_extractor", "researcher", "coder", "general", "custom"] :: [Text]),
"description" .= ("Subagent role determining tools and model" :: Text)
],
"task"
@@ -1019,6 +1079,34 @@ spawnSubagentTool keys =
[ "type" .= ("string" :: Text),
"description" .= ("Code namespace like 'Omni/Agent/Subagent' (required for coder role)" :: Text)
],
+ "custom_role_name"
+ .= Aeson.object
+ [ "type" .= ("string" :: Text),
+ "description" .= ("Name for custom role (when role=custom)" :: Text)
+ ],
+ "tools"
+ .= Aeson.object
+ [ "type" .= ("array" :: Text),
+ "items" .= Aeson.object ["type" .= ("string" :: Text)],
+ "description" .= ("Override default tools with specific tool names" :: Text)
+ ],
+ "system_prompt"
+ .= Aeson.object
+ [ "type" .= ("string" :: Text),
+ "description" .= ("Additional system prompt instructions for this subagent" :: Text)
+ ],
+ "guardrails"
+ .= Aeson.object
+ [ "type" .= ("object" :: Text),
+ "description" .= ("Per-spawn guardrails overriding defaults" :: Text),
+ "properties"
+ .= Aeson.object
+ [ "max_cost_cents" .= Aeson.object ["type" .= ("number" :: Text)],
+ "max_tokens" .= Aeson.object ["type" .= ("integer" :: Text)],
+ "max_iterations" .= Aeson.object ["type" .= ("integer" :: Text)],
+ "max_duplicate_tool_calls" .= Aeson.object ["type" .= ("integer" :: Text)]
+ ]
+ ],
"confirmed"
.= Aeson.object
[ "type" .= ("boolean" :: Text),
@@ -1071,6 +1159,7 @@ formatApprovalRequest config =
DataExtractor -> "DataExtractor"
Researcher -> "Researcher"
Coder -> "Coder"
+ General -> "General"
CustomRole name -> name
estimatedTime :: Int
estimatedTime = subagentTimeout config `div` 60
@@ -1192,7 +1281,9 @@ spawnSubagentToolWithApproval keys chatId onApprovalNeeded =
<> "do NOT say 'spawned' or 'started', say 'requested' or 'awaiting approval'. "
<> "Available roles: web_crawler (fast web research), code_reviewer (thorough code analysis), "
<> "data_extractor (structured data extraction), researcher (general research), "
- <> "coder (hardened coding with init/verify/commit - requires namespace and context).",
+ <> "coder (hardened coding with init/verify/commit - requires namespace and context), "
+ <> "general (balanced tools for non-specialized tasks), "
+ <> "custom (use custom_role_name and specify tools).",
Engine.toolJsonSchema =
Aeson.object
[ "type" .= ("object" :: Text),
@@ -1201,7 +1292,7 @@ spawnSubagentToolWithApproval keys chatId onApprovalNeeded =
[ "role"
.= Aeson.object
[ "type" .= ("string" :: Text),
- "enum" .= (["web_crawler", "code_reviewer", "data_extractor", "researcher", "coder"] :: [Text]),
+ "enum" .= (["web_crawler", "code_reviewer", "data_extractor", "researcher", "coder", "general", "custom"] :: [Text]),
"description" .= ("Subagent role determining tools and model" :: Text)
],
"task"
@@ -1233,6 +1324,34 @@ spawnSubagentToolWithApproval keys chatId onApprovalNeeded =
.= Aeson.object
[ "type" .= ("string" :: Text),
"description" .= ("Code namespace like 'Omni/Agent/Subagent' (required for coder role)" :: Text)
+ ],
+ "custom_role_name"
+ .= Aeson.object
+ [ "type" .= ("string" :: Text),
+ "description" .= ("Name for custom role (when role=custom)" :: Text)
+ ],
+ "tools"
+ .= Aeson.object
+ [ "type" .= ("array" :: Text),
+ "items" .= Aeson.object ["type" .= ("string" :: Text)],
+ "description" .= ("Override default tools with specific tool names" :: Text)
+ ],
+ "system_prompt"
+ .= Aeson.object
+ [ "type" .= ("string" :: Text),
+ "description" .= ("Additional system prompt instructions for this subagent" :: Text)
+ ],
+ "guardrails"
+ .= Aeson.object
+ [ "type" .= ("object" :: Text),
+ "description" .= ("Per-spawn guardrails overriding defaults" :: Text),
+ "properties"
+ .= Aeson.object
+ [ "max_cost_cents" .= Aeson.object ["type" .= ("number" :: Text)],
+ "max_tokens" .= Aeson.object ["type" .= ("integer" :: Text)],
+ "max_iterations" .= Aeson.object ["type" .= ("integer" :: Text)],
+ "max_duplicate_tool_calls" .= Aeson.object ["type" .= ("integer" :: Text)]
+ ]
]
],
"required" .= (["role", "task"] :: [Text])
@@ -1252,6 +1371,7 @@ executeSpawnWithApproval _keys chatId onApprovalNeeded v =
DataExtractor -> "data_extractor"
Researcher -> "researcher"
Coder -> "coder"
+ General -> "general"
CustomRole name -> name
estimatedMins = subagentTimeout config `div` 60
maxCost = subagentMaxCost config
diff --git a/Omni/Agent/Telegram.hs b/Omni/Agent/Telegram.hs
index 72cbd6c..2bf7aed 100644
--- a/Omni/Agent/Telegram.hs
+++ b/Omni/Agent/Telegram.hs
@@ -232,6 +232,25 @@ telegramSystemPromptFallback =
"- use this for reminders ('remind me in 2 hours'), follow-ups, or multi-part responses",
"- you can list pending messages with 'list_pending_messages' and cancel with 'cancel_message'",
"",
+ "## subagent delegation",
+ "",
+ "delegate external actions to subagents rather than executing them directly.",
+ "this allows you to monitor progress, debug failures, and respawn with adjusted parameters.",
+ "",
+ "when to use subagents:",
+ "- code changes: spawn coder subagent (requires namespace and context)",
+ "- research tasks: spawn researcher subagent",
+ "- web scraping: spawn webcrawler subagent",
+ "- general multi-step tasks: spawn general subagent",
+ "- custom needs: spawn custom role with specific tools",
+ "",
+ "customize each spawn with:",
+ "- tools: override default tools with specific tool names",
+ "- system_prompt: additional instructions for this specific task",
+ "- guardrails: per-spawn limits (max_cost_cents, max_tokens, max_iterations)",
+ "",
+ "if a subagent fails, analyze the failure, adjust parameters, and respawn.",
+ "",
"## podcastitlater context",
"",
"you have access to the PodcastItLater codebase (a product Ben is building) via read_file:",