diff options
Diffstat (limited to 'Omni/Agent')
| -rw-r--r-- | Omni/Agent/Subagent.hs | 134 | ||||
| -rw-r--r-- | Omni/Agent/Telegram.hs | 19 |
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:", |
