Skip to content

feat(agent): builder tools — triggers, cron, find_triggers (#4919)#4928

Closed
mmabrouk wants to merge 1 commit into
big-agentsfrom
feat/agent-builder-tools-4919
Closed

feat(agent): builder tools — triggers, cron, find_triggers (#4919)#4928
mmabrouk wants to merge 1 commit into
big-agentsfrom
feat/agent-builder-tools-4919

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Jun 29, 2026

Copy link
Copy Markdown
Member

Context

An agent building itself needs to set up cron schedules, subscribe to external events, and discover what triggers are available for a given use case. None of those operations existed in the platform op catalog or the API. Without them, the set-up-triggers skill (PR #4924) names tools the agent cannot call.

Design: docs/design/agent-workflows/projects/agent-builder-capabilities/

Changes

SDK — op_catalog.py: Seven new platform ops. Mutation ops default to approval; read ops default to allow.

Op Endpoint Default permission
find_triggers POST /api/triggers/discover allow
create_trigger / create_cron POST /api/triggers/subscriptions/ / schedules/ ask, approval
delete_trigger / delete_cron DELETE /api/triggers/subscriptions/{id} / schedules/{id} ask, approval
list_triggers / list_crons GET /api/triggers/subscriptions/ / schedules/ allow

Destinations (data.references, data.selector) are bound from run context ($ctx) and stripped from the model-facing schema. The model cannot route a trigger to an arbitrary target.

API — triggers router and service: New POST /triggers/discover endpoint. Accepts use_cases (list of keyword fragments), provider, and limit_alternatives. The service method discover_triggers tokenizes each use-case string, scores every catalog event by keyword overlap, and returns the top match per use case plus alternatives, connection state, and guidance.

New DTOs in dtos.py under the # Trigger discovery section: TriggerCapabilitiesResult, TriggerCapability, DiscoveredTriggerEvent, TriggerDiscoveryConnectionState, TriggerConnectionRequirement, TriggerConnectAffordance, TriggerDiscoveryGuidance.

TS runner (services/agent/):

  • protocol.tsDELETE added to DirectCallMethod union (required for delete ops that use paths like /triggers/{id}).
  • direct.tsdirectCallUrl now substitutes path parameters (e.g., {trigger_id}) from the assembled body params before the call.
  • relay.ts — Assembles the body before computing the URL so path param substitution has access to all params.

Scope / risk

find_triggers does keyword matching in-process against the Composio trigger catalog. It is not a semantic search. The quality of matches depends on overlap between the user's words and the event names/descriptions in the catalog. The _DISCOVERY_STOPWORDS set is a first-pass filter; it may need tuning.

The relay.ts change (assemble body before directCallUrl) is a correctness fix, but it is a behavior change to the relay path. Verify that path-param substitution does not incorrectly consume fields intended as request body keys.

The DELETE method addition in direct.ts opens delete calls to any direct-call op, not just trigger ops. Confirm the allowlist and path-param logic are correct before merging.

The new platform ops (create_trigger, delete_trigger, etc.) are model-facing. The input schemas, approval defaults, and context bindings are load-bearing for safety. These deserve a careful read.

Tests

  • SDK Python (op_catalog): 18/18 passed (Codex-reported).
  • API Python (triggers discovery): 6/6 passed (Codex-reported).
  • TS typecheck (tsc --noEmit): clean.
  • TS vitest (tool-direct.test.ts): 42/42 passed (Codex-reported).
  • ruff format/check: clean.

How to QA

Prerequisites: local dev stack with the triggers service running (EE edition with Composio configured).

Steps:

  1. Call POST /api/triggers/discover with {"use_cases": ["new GitHub pull request"]} and a valid project API key.
  2. Confirm the response includes a capabilities list with at least one entry whose event.integration is GITHUB or similar.
  3. Check the connections list reflects the current connection state for that integration.

Automated tests:

cd api && uv run python -m pytest oss/tests/pytest/unit/triggers/test_triggers_discovery.py -v
cd sdks/python && uv run python -m pytest oss/tests/pytest/unit/agents/platform/test_op_catalog.py -v
cd services/agent && npx vitest run tests/unit/tool-direct.test.ts

Edge cases:

  • Call POST /triggers/discover with use_cases: [""] (empty string). Expect a 422 validation error from the _require_use_cases validator.
  • Use a DELETE direct call op. Confirm the path parameter is substituted correctly and the body does not include the substituted field as a body key.
  • Call list_triggers / list_crons from an agent run and confirm the routing context binding ($ctx) fills the destination without any model-supplied value.

https://claude.ai/code/session_01GYo3UEfvsZpncagqb28Mbc

…ign)

- Add trigger/cron platform ops to op_catalog.py: create_trigger,
  delete_trigger, list_triggers, create_cron, delete_cron, list_crons,
  find_triggers; each wired as direct-call tools with self/target ctx
  bindings and approval defaults.
- Extend find_triggers backend: new GET /triggers/discover endpoint
  (triggers router + service) returning event details and connection
  readiness per use-case.
- Extend SDK tools.models with trigger schema types.
- Add DELETE to the direct-call method allowlist in protocol.ts and
  direct.ts; add path-parameter substitution in directCallUrl so
  DELETE /triggers/{id} receives the id from the assembled body.
- relay.ts: assemble body before constructing URL (needed by path params).
- Tests: 18/18 op_catalog tests pass; 6/6 trigger discovery tests pass;
  42/42 TS direct-call unit tests pass.
@vercel

vercel Bot commented Jun 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jun 29, 2026 7:23am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added a trigger discovery endpoint to help find matching triggers from one or more use cases, including readiness status, alternatives, and connection guidance.
    • Expanded agent platform tools to support trigger discovery, schedule/subscription actions, and direct calls with path parameters.
    • Added support for DELETE requests in direct tool calls.
  • Bug Fixes

    • Improved validation for trigger discovery input so empty or malformed use cases are rejected.
    • Strengthened path-parameter handling to safely substitute and encode values in direct calls.

Walkthrough

Adds a POST /triggers/discover endpoint backed by keyword-based event-catalog search and connection-state resolution. Extends agent platform-op catalog with trigger, schedule, and subscription tools. Adds DELETE HTTP method support and path-parameter substitution across the Python SDK and TypeScript agent service. Includes design docs for agent builder capabilities.

Changes

Trigger Discovery Backend

Layer / File(s) Summary
Discovery DTOs
api/oss/src/core/triggers/dtos.py, api/oss/src/apis/fastapi/triggers/models.py
Adds TriggerDiscoveryConnectionState enum and Pydantic models for discovered events, alternatives, capabilities, connection requirements, and result aggregation. TriggerDiscoveryQuery validates use_cases (list, non-empty after trimming) with provider and limit_alternatives fields; TriggerDiscoveryResponse aliases TriggerCapabilitiesResult.
Discovery service logic
api/oss/src/core/triggers/service.py
Adds module-level constants, _discovery_terms tokenizer, _score_trigger_match scorer, _candidate_integrations/_candidate_events pagers with deduplication and fallback, _trigger_discovery_connection_state (READY/NEEDS_AUTH/NEEDS_INPUT with affordances), _trigger_connection_auth_state, and the main discover_triggers orchestration method.
POST /discover endpoint
api/oss/src/apis/fastapi/triggers/router.py
Registers POST /discover in TriggersRouter.__init__, adds discover_triggers handler with VIEW_TRIGGERS permission check delegating to the service.
Discovery tests
api/oss/tests/pytest/unit/triggers/test_triggers_discovery.py
Unit tests for result shape, connection state inference, router argument forwarding, route registration at /discover, and TriggerDiscoveryQuery validation rejection.

Agent Platform Tools and DELETE Support

Layer / File(s) Summary
DELETE method across tool stack
sdks/python/agenta/sdk/agents/tools/models.py, sdks/python/agenta/sdk/agents/platform/op_catalog.py, services/agent/src/protocol.ts, services/agent/src/tools/direct.ts
Expands Literal method types in ToolCall, PlatformOp, ResolvedToolSpec, callDirect, and directCallUrl to include DELETE.
Path-parameter substitution in directCallUrl
services/agent/src/tools/direct.ts, services/agent/src/tools/relay.ts, services/agent/tests/unit/tool-direct.test.ts
Adds helpers to resolve and URL-encode {placeholder} segments in call.path, enforcing scalar types and throwing on missing params; updates directCallUrl signature; fixes assembleBody/directCallUrl evaluation order in relay; adds positive/negative test cases.
Platform-op catalog entries
sdks/python/agenta/sdk/agents/platform/op_catalog.py, sdks/python/oss/tests/pytest/unit/agents/platform/test_op_catalog.py
Adds JSON-schema constants and PLATFORM_OPS entries for find_triggers, create/list/remove/pause/resume schedule and subscription ops, test_subscription, list_connections, and list_deliveries with context bindings, args_into routing, and approval flags. Tests assert method/path, permission gating, self-target context bindings, and schema sanitization.

Agent Builder Capabilities Design Docs

Layer / File(s) Summary
Design README and status
docs/design/agent-workflows/projects/agent-builder-capabilities/*
Full design document covering tool contracts, find_triggers spec, build flow, overlay delivery, ownership split, and status tracker with open questions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.16% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title names the main builder-tools work and specifically mentions triggers and cron discovery, which matches the PR's core changes.
Description check ✅ Passed The description directly matches the new trigger/cron platform ops, discovery API, SDK updates, and direct-call changes in the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/agent-builder-tools-4919

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mmabrouk mmabrouk added the needs-review Agent updated; awaiting Mahmoud's review label Jun 29, 2026
@mmabrouk

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mmabrouk

Copy link
Copy Markdown
Member Author

🤖 The AI agent says:

What to review on this PR:

  1. Platform op input schemas and context bindings (op_catalog.py). This is the load-bearing safety surface. For each new op (create_trigger, create_cron, delete_trigger, delete_cron), confirm that data.references and data.selector are bound from $ctx and stripped from the model-facing schema. The model must not be able to route a trigger to an arbitrary target.

  2. relay.ts body-before-URL ordering change. The PR assembles the request body before calling directCallUrl so path parameters are available for substitution. Confirm that assembling the body earlier does not change any observable behavior for existing non-DELETE ops (none of them use path params today, but the order change touches the general path).

  3. direct.ts DELETE allowlist. Adding DELETE to the DirectCallMethod union opens delete calls to any op that uses it. Confirm the path-param substitution logic extracts {id}-style params correctly and removes the substituted key from the body before it is sent as the request body.

  4. discover_triggers keyword scoring (service.py). The _score_trigger_match function is in-process keyword overlap against the Composio catalog. There are no tests for the scoring logic itself, only for the endpoint shape. Consider whether a low-confidence match (score of 1 on a common word) could return a wrong integration with high confidence to the model.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
docs/design/agent-workflows/projects/agent-builder-capabilities/README.md (1)

209-210: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clarify what "already ship" means for delete endpoints.

The design states that remove/pause/resume tools map to "delete and start/stop endpoints that already ship." While the API routes (DELETE /api/triggers/schedules/{id} etc.) existed in the backend, the agent tool stack did not support the DELETE HTTP method until this PR (added to direct.ts, relay.ts, protocol.ts, and Python SDK ToolCall). Consider adding a parenthetical noting that the agent-side DELETE binding is new in this project, to prevent readers from assuming the agent integration was already complete.

api/oss/tests/pytest/unit/triggers/test_triggers_discovery.py (1)

58-145: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a no-match regression test for discovery.

The service has an explicit no-match branch, but this file only covers positive matches and connection-state inference. A case where every candidate scores 0 would lock in the expected note/empty-event behavior and would have caught the current fallback bug.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 384e1e57-ea61-4f15-9312-87cda2bffffe

📥 Commits

Reviewing files that changed from the base of the PR and between 0755d86 and 4fb9334.

⛔ Files ignored due to path filters (1)
  • services/agent/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • api/oss/src/apis/fastapi/triggers/models.py
  • api/oss/src/apis/fastapi/triggers/router.py
  • api/oss/src/core/triggers/dtos.py
  • api/oss/src/core/triggers/service.py
  • api/oss/tests/pytest/unit/triggers/test_triggers_discovery.py
  • docs/design/agent-workflows/projects/agent-builder-capabilities/README.md
  • docs/design/agent-workflows/projects/agent-builder-capabilities/status.md
  • sdks/python/agenta/sdk/agents/platform/op_catalog.py
  • sdks/python/agenta/sdk/agents/tools/models.py
  • sdks/python/oss/tests/pytest/unit/agents/platform/test_op_catalog.py
  • services/agent/src/protocol.ts
  • services/agent/src/tools/direct.ts
  • services/agent/src/tools/relay.ts
  • services/agent/tests/unit/tool-direct.test.ts

Comment on lines +71 to +83
use_cases: List[str]
provider: str = TriggerProviderKind.COMPOSIO.value
limit_alternatives: int = Field(default=3, ge=0)

@field_validator("use_cases", mode="before")
@classmethod
def _require_use_cases(cls, value: Any) -> List[str]:
if not isinstance(value, list):
raise ValueError("use_cases must be a list of non-empty fragments")
items = [str(v).strip() for v in value if str(v).strip()]
if not items:
raise ValueError("use_cases must contain at least one non-empty fragment")
return items

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the discovery request schema strict.

provider accepts any string, and _require_use_cases() stringifies arbitrary list items (123, {}) instead of rejecting them. That turns malformed payloads into meaningless searches instead of a 422.

Suggested fix
 class TriggerDiscoveryQuery(BaseModel):
     """Request body for ``POST /triggers/discover``."""
 
     use_cases: List[str]
-    provider: str = TriggerProviderKind.COMPOSIO.value
+    provider: TriggerProviderKind = TriggerProviderKind.COMPOSIO
     limit_alternatives: int = Field(default=3, ge=0)
 
     `@field_validator`("use_cases", mode="before")
     `@classmethod`
     def _require_use_cases(cls, value: Any) -> List[str]:
         if not isinstance(value, list):
             raise ValueError("use_cases must be a list of non-empty fragments")
-        items = [str(v).strip() for v in value if str(v).strip()]
+        if any(not isinstance(v, str) for v in value):
+            raise ValueError("use_cases must be a list of strings")
+        items = [v.strip() for v in value if v.strip()]
         if not items:
             raise ValueError("use_cases must contain at least one non-empty fragment")
         return items
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
use_cases: List[str]
provider: str = TriggerProviderKind.COMPOSIO.value
limit_alternatives: int = Field(default=3, ge=0)
@field_validator("use_cases", mode="before")
@classmethod
def _require_use_cases(cls, value: Any) -> List[str]:
if not isinstance(value, list):
raise ValueError("use_cases must be a list of non-empty fragments")
items = [str(v).strip() for v in value if str(v).strip()]
if not items:
raise ValueError("use_cases must contain at least one non-empty fragment")
return items
use_cases: List[str]
provider: TriggerProviderKind = TriggerProviderKind.COMPOSIO
limit_alternatives: int = Field(default=3, ge=0)
`@field_validator`("use_cases", mode="before")
`@classmethod`
def _require_use_cases(cls, value: Any) -> List[str]:
if not isinstance(value, list):
raise ValueError("use_cases must be a list of non-empty fragments")
if any(not isinstance(v, str) for v in value):
raise ValueError("use_cases must be a list of strings")
items = [v.strip() for v in value if v.strip()]
if not items:
raise ValueError("use_cases must contain at least one non-empty fragment")
return items

return items


TriggerDiscoveryResponse = TriggerCapabilitiesResult

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Wrap /discover in a normal API response envelope.

Aliasing the response to the core DTO makes this endpoint the odd one out in the router: the rest return {count, ...} envelopes, but this one exposes the core result directly. Please define an explicit API response model here and wrap the service DTO at the route boundary. As per coding guidelines, "Define explicit request and response models in models.py, include count plus payload in response envelopes."

Source: Coding guidelines

Comment on lines +400 to +406
async def _discover_events_for_use_case(
self,
*,
provider_key: str,
use_case: str,
limit_alternatives: int,
) -> List[Tuple[int, TriggerCatalogEvent, TriggerCatalogIntegration]]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use a named DTO for discovery matches instead of tuples.

Returning List[Tuple[int, TriggerCatalogEvent, TriggerCatalogIntegration]] from a method in core/**/service.py violates the core service contract and leaks positional tuple unpacking through the discovery flow. As per coding guidelines, "Do not return raw dicts or tuples from service methods or clients; define named DTOs in core/{domain}/dtos.py instead." and "Service methods must return typed DTOs (Pydantic BaseModel subclasses), not raw dicts, tuples, or Any."

Source: Coding guidelines

Comment on lines +427 to +439
matches.append(
(
_score_trigger_match(
use_case=use_case,
event=event,
integration=integration,
),
event,
integration,
)
)

return sorted(matches, key=lambda item: item[0], reverse=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Drop zero-score candidates before returning matches.

When _candidate_integrations() / _candidate_events() fall back to unfiltered catalog pages, this helper still appends score-0 events. discover_triggers() then surfaces an arbitrary event instead of hitting its no-match path.

Suggested fix
             for event in events:
                 key = (event.integration or integration.key, event.key)
                 if key in seen:
                     continue
+                score = _score_trigger_match(
+                    use_case=use_case,
+                    event=event,
+                    integration=integration,
+                )
+                if score <= 0:
+                    continue
                 seen.add(key)
-                matches.append(
-                    (
-                        _score_trigger_match(
-                            use_case=use_case,
-                            event=event,
-                            integration=integration,
-                        ),
-                        event,
-                        integration,
-                    )
-                )
+                matches.append((score, event, integration))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
matches.append(
(
_score_trigger_match(
use_case=use_case,
event=event,
integration=integration,
),
event,
integration,
)
)
return sorted(matches, key=lambda item: item[0], reverse=True)
score = _score_trigger_match(
use_case=use_case,
event=event,
integration=integration,
)
if score <= 0:
continue
matches.append(
(
score,
event,
integration,
)
)
return sorted(matches, key=lambda item: item[0], reverse=True)

Comment on lines +423 to +433
_EMPTY_INPUT_SCHEMA: Dict[str, Any] = {"type": "object", "properties": {}}
_TRIGGER_ID_INPUT_SCHEMA: Dict[str, Any] = {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The schedule or subscription id returned by the list tools.",
}
},
"required": ["id"],
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Close the no-arg and id-only schemas.

_EMPTY_INPUT_SCHEMA and _TRIGGER_ID_INPUT_SCHEMA currently leave additionalProperties open, so list_connections plus the POST pause_* / resume_* ops expose arbitrary extra body fields to the backend. That makes these tools depend on server-side internals instead of the cataloged request shape.

Proposed fix
-_EMPTY_INPUT_SCHEMA: Dict[str, Any] = {"type": "object", "properties": {}}
+_EMPTY_INPUT_SCHEMA: Dict[str, Any] = {
+    "type": "object",
+    "properties": {},
+    "additionalProperties": False,
+}
 _TRIGGER_ID_INPUT_SCHEMA: Dict[str, Any] = {
     "type": "object",
     "properties": {
         "id": {
             "type": "string",
             "description": "The schedule or subscription id returned by the list tools.",
         }
     },
     "required": ["id"],
+    "additionalProperties": False,
 }

Also applies to: 531-539, 568-603

@mmabrouk

Copy link
Copy Markdown
Member Author

🤖 The AI agent says: Superseded by #4931 — same content, rebuilt as a clean GitButler stack. Closing this one.

@mmabrouk mmabrouk closed this Jun 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-review Agent updated; awaiting Mahmoud's review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant