swytchcode-runtime 1.0.0__tar.gz → 1.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {swytchcode_runtime-1.0.0/swytchcode_runtime.egg-info → swytchcode_runtime-1.0.2}/PKG-INFO +52 -17
  2. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/README.md +51 -16
  3. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/pyproject.toml +1 -1
  4. swytchcode_runtime-1.0.2/swytchcode_runtime/__init__.py +30 -0
  5. swytchcode_runtime-1.0.2/swytchcode_runtime/client.py +243 -0
  6. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/exec.py +7 -1
  7. swytchcode_runtime-1.0.2/swytchcode_runtime/providers/__init__.py +27 -0
  8. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/providers/base.py +1 -1
  9. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/providers/openai_agents.py +1 -1
  10. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/providers/vercel.py +2 -2
  11. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/schema.py +13 -9
  12. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2/swytchcode_runtime.egg-info}/PKG-INFO +52 -17
  13. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime.egg-info/SOURCES.txt +3 -1
  14. swytchcode_runtime-1.0.2/tests/test_alias.py +42 -0
  15. swytchcode_runtime-1.0.2/tests/test_schema_routing.py +62 -0
  16. swytchcode_runtime-1.0.0/swytchcode_runtime/__init__.py +0 -24
  17. swytchcode_runtime-1.0.0/swytchcode_runtime/client.py +0 -133
  18. swytchcode_runtime-1.0.0/swytchcode_runtime/providers/__init__.py +0 -15
  19. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/LICENSE.md +0 -0
  20. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/setup.cfg +0 -0
  21. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/cli.py +0 -0
  22. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/discover.py +0 -0
  23. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/errors.py +0 -0
  24. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/manage.py +0 -0
  25. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/providers/anthropic.py +0 -0
  26. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/providers/crewai.py +0 -0
  27. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/providers/langgraph.py +0 -0
  28. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime.egg-info/dependency_links.txt +0 -0
  29. {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swytchcode-runtime
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Thin runtime wrapper around the Swytchcode CLI
5
5
  Author: Swytchcode
6
6
  License: MIT
@@ -147,39 +147,74 @@ def create_account(input: dict) -> any:
147
147
 
148
148
  ## Agentic workflows (framework integrations)
149
149
 
150
- On top of `exec`, the runtime exposes a small agentic surface that turns Swytchcode tools
151
- into the native tool objects each agent framework expects. The Swytchcode part is the same
152
- two lines regardless of framework:
150
+ For full, production-ready examples across all major frameworks, check out the [Swytchcode Examples Repository](https://github.com/swytchcodehq/swytchcode-examples).
153
151
 
152
+ On top of `exec`, the runtime exposes a small agentic surface that turns Swytchcode tools into the native tool objects each agent framework expects.
153
+
154
+ ### Quickstart: Anthropic SDK
155
+
156
+ Here is a clean example of building a simple agent using the Anthropic SDK. We use `python-dotenv` to load environment variables (like `ANTHROPIC_API_KEY`).
157
+
158
+ **Installation:**
159
+ ```bash
160
+ pip install swytchcode-runtime anthropic python-dotenv
161
+ ```
162
+ *(Note: You only need to install the SDK for the framework you are actually using. You **do not** need to install `openai-agents` or `langchain` if you are only using Anthropic. The `swytchcode-runtime` isolates these dependencies via lazy loading.)*
163
+
164
+ **Example:**
154
165
  ```python
166
+ import os
167
+ from dotenv import load_dotenv
168
+ import anthropic
155
169
  from swytchcode_runtime import Swytchcode
156
170
  from swytchcode_runtime.providers.anthropic import AnthropicProvider
157
171
 
158
- swx = Swytchcode(provider=AnthropicProvider())
159
- tools = swx.tools.get(toolkits=["stripe"]) # framework-native tools, full param schemas
172
+ load_dotenv() # Loads .env automatically
173
+
174
+ def run_agent():
175
+ client = anthropic.Anthropic()
176
+
177
+ # 1. Initialize Swytchcode with the Anthropic provider
178
+ swx = Swytchcode(provider=AnthropicProvider())
179
+
180
+ # 2. Fetch the tools you want your agent to use (e.g., Stripe tools)
181
+ tools = swx.tools.get(toolkits=["stripe"])
182
+
183
+ # 3. Pass them to Claude
184
+ response = client.messages.create(
185
+ model="claude-3-5-sonnet-latest",
186
+ max_tokens=1024,
187
+ tools=tools,
188
+ messages=[{"role": "user", "content": "Refund charge ch_123 for $20."}],
189
+ )
190
+
191
+ print(response)
192
+
193
+ if __name__ == "__main__":
194
+ run_agent()
160
195
  ```
161
196
 
162
- ### Selecting tools — `swx.tools.get(...)`
197
+ ### Selecting tools - `swx.tools.get(...)`
163
198
 
164
199
  Pass exactly one selector; IDs resolve against your local Swytchcode state and remote search:
165
200
 
166
- - `toolkits=["stripe"]` — every enabled tool whose integration matches a toolkit.
167
- - `tools=["charges.charge.create"]` — explicit canonical IDs.
168
- - `search="refund a charge"` — natural-language discovery (via `swytchcode discover`).
201
+ - `toolkits=["stripe"]` - every enabled tool whose integration matches a toolkit.
202
+ - `tools=["charges.charge.create"]` - explicit canonical IDs.
203
+ - `search="refund a charge"` - natural-language discovery (via `swytchcode discover`).
169
204
 
170
- Each returned tool carries a **full input schema** — every field is surfaced to the model, with
171
- only the truly-required ones marked `required` — and an `execute` callback that runs `swytchcode
205
+ Each returned tool carries a **full input schema** - every field is surfaced to the model, with
206
+ only the truly-required ones marked `required` - and an `execute` callback that runs `swytchcode
172
207
  exec` for you (empty optional values are stripped before the call so APIs like Stripe don't reject them).
173
208
 
174
209
  ### Supported providers
175
210
 
176
211
  | Framework | Import | Who runs the tool loop |
177
212
  |-----------|--------|------------------------|
178
- | Anthropic Claude | `providers.anthropic.AnthropicProvider` | you (Messages API + `swx.handle_tool_calls`) |
179
- | OpenAI Agents SDK | `providers.openai_agents.OpenAIAgentsProvider` | the SDK |
180
- | Vercel AI SDK | `providers.vercel.VercelProvider` | the SDK |
181
- | LangGraph | `providers.langgraph.LangGraphProvider` | the prebuilt agent |
182
- | CrewAI | `providers.crewai.CrewAIProvider` | the crew |
213
+ | Anthropic Claude | `from swytchcode_runtime.providers.anthropic import AnthropicProvider` | you (Messages API + `swx.handle_tool_calls`) |
214
+ | OpenAI Agents SDK | `from swytchcode_runtime.providers.openai_agents import OpenAIAgentsProvider` | the SDK |
215
+ | Vercel AI SDK | `from swytchcode_runtime.providers.vercel import VercelProvider` | the SDK |
216
+ | LangGraph | `from swytchcode_runtime.providers.langgraph import LangGraphProvider` | the prebuilt agent |
217
+ | CrewAI | `from swytchcode_runtime.providers.crewai import CrewAIProvider` | the crew |
183
218
 
184
219
  ### Non-agentic APIs (Anthropic Messages)
185
220
 
@@ -129,39 +129,74 @@ def create_account(input: dict) -> any:
129
129
 
130
130
  ## Agentic workflows (framework integrations)
131
131
 
132
- On top of `exec`, the runtime exposes a small agentic surface that turns Swytchcode tools
133
- into the native tool objects each agent framework expects. The Swytchcode part is the same
134
- two lines regardless of framework:
132
+ For full, production-ready examples across all major frameworks, check out the [Swytchcode Examples Repository](https://github.com/swytchcodehq/swytchcode-examples).
135
133
 
134
+ On top of `exec`, the runtime exposes a small agentic surface that turns Swytchcode tools into the native tool objects each agent framework expects.
135
+
136
+ ### Quickstart: Anthropic SDK
137
+
138
+ Here is a clean example of building a simple agent using the Anthropic SDK. We use `python-dotenv` to load environment variables (like `ANTHROPIC_API_KEY`).
139
+
140
+ **Installation:**
141
+ ```bash
142
+ pip install swytchcode-runtime anthropic python-dotenv
143
+ ```
144
+ *(Note: You only need to install the SDK for the framework you are actually using. You **do not** need to install `openai-agents` or `langchain` if you are only using Anthropic. The `swytchcode-runtime` isolates these dependencies via lazy loading.)*
145
+
146
+ **Example:**
136
147
  ```python
148
+ import os
149
+ from dotenv import load_dotenv
150
+ import anthropic
137
151
  from swytchcode_runtime import Swytchcode
138
152
  from swytchcode_runtime.providers.anthropic import AnthropicProvider
139
153
 
140
- swx = Swytchcode(provider=AnthropicProvider())
141
- tools = swx.tools.get(toolkits=["stripe"]) # framework-native tools, full param schemas
154
+ load_dotenv() # Loads .env automatically
155
+
156
+ def run_agent():
157
+ client = anthropic.Anthropic()
158
+
159
+ # 1. Initialize Swytchcode with the Anthropic provider
160
+ swx = Swytchcode(provider=AnthropicProvider())
161
+
162
+ # 2. Fetch the tools you want your agent to use (e.g., Stripe tools)
163
+ tools = swx.tools.get(toolkits=["stripe"])
164
+
165
+ # 3. Pass them to Claude
166
+ response = client.messages.create(
167
+ model="claude-3-5-sonnet-latest",
168
+ max_tokens=1024,
169
+ tools=tools,
170
+ messages=[{"role": "user", "content": "Refund charge ch_123 for $20."}],
171
+ )
172
+
173
+ print(response)
174
+
175
+ if __name__ == "__main__":
176
+ run_agent()
142
177
  ```
143
178
 
144
- ### Selecting tools — `swx.tools.get(...)`
179
+ ### Selecting tools - `swx.tools.get(...)`
145
180
 
146
181
  Pass exactly one selector; IDs resolve against your local Swytchcode state and remote search:
147
182
 
148
- - `toolkits=["stripe"]` — every enabled tool whose integration matches a toolkit.
149
- - `tools=["charges.charge.create"]` — explicit canonical IDs.
150
- - `search="refund a charge"` — natural-language discovery (via `swytchcode discover`).
183
+ - `toolkits=["stripe"]` - every enabled tool whose integration matches a toolkit.
184
+ - `tools=["charges.charge.create"]` - explicit canonical IDs.
185
+ - `search="refund a charge"` - natural-language discovery (via `swytchcode discover`).
151
186
 
152
- Each returned tool carries a **full input schema** — every field is surfaced to the model, with
153
- only the truly-required ones marked `required` — and an `execute` callback that runs `swytchcode
187
+ Each returned tool carries a **full input schema** - every field is surfaced to the model, with
188
+ only the truly-required ones marked `required` - and an `execute` callback that runs `swytchcode
154
189
  exec` for you (empty optional values are stripped before the call so APIs like Stripe don't reject them).
155
190
 
156
191
  ### Supported providers
157
192
 
158
193
  | Framework | Import | Who runs the tool loop |
159
194
  |-----------|--------|------------------------|
160
- | Anthropic Claude | `providers.anthropic.AnthropicProvider` | you (Messages API + `swx.handle_tool_calls`) |
161
- | OpenAI Agents SDK | `providers.openai_agents.OpenAIAgentsProvider` | the SDK |
162
- | Vercel AI SDK | `providers.vercel.VercelProvider` | the SDK |
163
- | LangGraph | `providers.langgraph.LangGraphProvider` | the prebuilt agent |
164
- | CrewAI | `providers.crewai.CrewAIProvider` | the crew |
195
+ | Anthropic Claude | `from swytchcode_runtime.providers.anthropic import AnthropicProvider` | you (Messages API + `swx.handle_tool_calls`) |
196
+ | OpenAI Agents SDK | `from swytchcode_runtime.providers.openai_agents import OpenAIAgentsProvider` | the SDK |
197
+ | Vercel AI SDK | `from swytchcode_runtime.providers.vercel import VercelProvider` | the SDK |
198
+ | LangGraph | `from swytchcode_runtime.providers.langgraph import LangGraphProvider` | the prebuilt agent |
199
+ | CrewAI | `from swytchcode_runtime.providers.crewai import CrewAIProvider` | the crew |
165
200
 
166
201
  ### Non-agentic APIs (Anthropic Messages)
167
202
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "swytchcode-runtime"
7
- version = "1.0.0"
7
+ version = "1.0.2"
8
8
  description = "Thin runtime wrapper around the Swytchcode CLI"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -0,0 +1,30 @@
1
+ """Thin runtime wrapper around the Swytchcode CLI."""
2
+
3
+ from .errors import SwytchcodeError, is_swytchcode_error
4
+ from .exec import exec_ as exec
5
+ from .client import Swytchcode
6
+
7
+ __all__ = [
8
+ "exec",
9
+ "Swytchcode",
10
+ "SwytchcodeError",
11
+ "is_swytchcode_error",
12
+ ]
13
+
14
+
15
+ def __getattr__(name):
16
+ """Lazy provider imports for backward compatibility."""
17
+ _provider_map = {
18
+ "AnthropicProvider": ("providers.anthropic", "AnthropicProvider"),
19
+ "OpenAIAgentsProvider": ("providers.openai_agents", "OpenAIAgentsProvider"),
20
+ "VercelProvider": ("providers.vercel", "VercelProvider"),
21
+ "LangGraphProvider": ("providers.langgraph", "LangGraphProvider"),
22
+ "CrewAIProvider": ("providers.crewai", "CrewAIProvider"),
23
+ }
24
+ if name in _provider_map:
25
+ module_path, attr = _provider_map[name]
26
+ import importlib
27
+
28
+ mod = importlib.import_module(f".{module_path}", __package__)
29
+ return getattr(mod, attr)
30
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,243 @@
1
+ """High-level agentic client on top of the existing exec()."""
2
+
3
+ from __future__ import annotations
4
+ from typing import Any, Optional
5
+
6
+ import re
7
+ import hashlib
8
+
9
+ from . import discover as _discover, schema as _schema, manage as _manage
10
+ from .exec import exec_ as _exec
11
+ from .providers.base import Provider, Tool
12
+
13
+
14
+ MAX_TOOL_NAME_LEN = 64 # OpenAI and Anthropic strict limit
15
+
16
+
17
+ def _make_alias(cid: str, taken: dict[str, str]) -> str:
18
+ base = re.sub(r"[^a-zA-Z0-9_-]", "_", cid)
19
+ existing = taken.get(base)
20
+ needs_hash = len(base) > MAX_TOOL_NAME_LEN or (existing and existing != cid)
21
+
22
+ if not needs_hash:
23
+ return base
24
+
25
+ h = hashlib.sha1(cid.encode("utf-8")).hexdigest()[:6]
26
+ keep = MAX_TOOL_NAME_LEN - 1 - len(h)
27
+ return base[:keep] + "_" + h
28
+
29
+
30
+ def _toolkit_matches(toolkit: str, integration: str) -> bool:
31
+ """Case-insensitive match of a toolkit name against an integration string.
32
+
33
+ integration has the shape "ProjectDisplayName.library_slug@version"
34
+ (e.g. "GitHub.github@1.1.4"). We compare against both the project name
35
+ and the library slug so that ``toolkits=["github"]`` matches regardless
36
+ of the capitalised display name.
37
+ """
38
+ tk = toolkit.lower()
39
+ # Strip version: "GitHub.github@1.1.4" → "GitHub.github"
40
+ at_idx = integration.find("@")
41
+ prefix = integration[:at_idx] if at_idx != -1 else integration
42
+ # Split project.library: "GitHub.github" → ("GitHub", "github")
43
+ dot_idx = prefix.find(".")
44
+ if dot_idx != -1:
45
+ project = prefix[:dot_idx].lower()
46
+ lib_slug = prefix[dot_idx + 1 :].lower()
47
+ else:
48
+ project = prefix.lower()
49
+ lib_slug = ""
50
+ return tk == project or tk == lib_slug or tk == prefix.lower()
51
+
52
+
53
+ def _strip_empty(obj: Any) -> Any:
54
+ """Recursively drop keys whose value is None or an empty string ("").
55
+
56
+ With 'expose all fields', agents often fill unused optional fields with "",
57
+ which APIs like Stripe reject ("empty values are an attempt to unset").
58
+ Only None and "" are dropped - 0, False, [], {} are preserved as meaningful.
59
+ """
60
+ if isinstance(obj, dict):
61
+ out = {}
62
+ for k, v in obj.items():
63
+ if v is None or (isinstance(v, str) and v == ""):
64
+ continue
65
+ out[k] = _strip_empty(v)
66
+ return out
67
+ if isinstance(obj, list):
68
+ return [_strip_empty(v) for v in obj]
69
+ return obj
70
+
71
+
72
+ def _split_by_location(inputs: Any, flat_args: dict) -> dict:
73
+ """Route flat args into body/params based on LOCATION metadata from wrekenfile."""
74
+ body = {}
75
+ params = {}
76
+ # inputs is the raw wrekenfile list-of-single-key-dicts with LOCATION, or a JSON schema dict
77
+ locations = {}
78
+
79
+ if isinstance(inputs, list):
80
+ for item in inputs:
81
+ if isinstance(item, dict):
82
+ for name, spec in item.items():
83
+ if isinstance(spec, dict):
84
+ loc = str(
85
+ spec.get("LOCATION", spec.get("location", "body"))
86
+ ).lower()
87
+ locations[name] = loc
88
+ elif isinstance(inputs, dict) and isinstance(inputs.get("properties"), dict):
89
+ for name, spec in inputs["properties"].items():
90
+ if isinstance(spec, dict):
91
+ loc = str(spec.get("LOCATION", spec.get("location", "body"))).lower()
92
+ locations[name] = loc
93
+
94
+ for key, val in flat_args.items():
95
+ loc = locations.get(key, "body")
96
+ if loc in ("path", "query"):
97
+ params[key] = val
98
+ else:
99
+ body[key] = val
100
+
101
+ result = {}
102
+ if body:
103
+ result["body"] = body
104
+ if params:
105
+ result["params"] = params
106
+ return result if result else {"body": flat_args}
107
+
108
+
109
+ class _Tools:
110
+ def __init__(self, client: "Swytchcode"):
111
+ self._c = client
112
+ # Maps a sanitized tool name (dots -> underscores) back to its canonical
113
+ # ID, populated as tools are built. Used to reverse names in
114
+ # handle_tool_calls without a lossy "_"->"." string replace.
115
+ self._name_to_cid: dict[str, str] = {}
116
+ self._cid_to_inputs: dict[str, Any] = {}
117
+
118
+ def get(self, *, toolkits=None, tools=None, search=None):
119
+ # Sort canonical IDs lexicographically before alias assignment.
120
+ # This ensures deterministic assignment order across runs, guaranteeing the
121
+ # same canonical ID always receives the exact same alias (and same hash if colliding).
122
+ ids = sorted(self._ids(toolkits, tools, search))
123
+ neutral = [self._tool(cid) for cid in ids]
124
+ p = self._c.provider
125
+ return p.format_tools(neutral) if p else neutral
126
+
127
+ def execute(self, canonical_id: str, args: dict, **options) -> Any:
128
+ final_args = dict(args)
129
+
130
+ # If args are flat (no body/params top-level keys), wrap them in body
131
+ # as expected by the Swytchcode CLI kernel (like in run-workflow.js)
132
+ if "body" not in final_args and "params" not in final_args:
133
+ raw_inputs = options.pop("_raw_inputs", None)
134
+ if raw_inputs is not None:
135
+ final_args = _split_by_location(raw_inputs, final_args)
136
+ else:
137
+ final_args = {"body": final_args}
138
+ else:
139
+ options.pop("_raw_inputs", None)
140
+
141
+ # Drop empty optional fields (None/"") from body & params so values an
142
+ # agent over-filled don't reach the API (e.g. Stripe rejects customer="").
143
+ if isinstance(final_args.get("body"), (dict, list)):
144
+ final_args["body"] = _strip_empty(final_args["body"])
145
+ if isinstance(final_args.get("params"), (dict, list)):
146
+ final_args["params"] = _strip_empty(final_args["params"])
147
+ # Forward exec options (dry_run, raw, allow_raw, cwd, env) to the CLI.
148
+ return _exec(canonical_id, final_args, **options)
149
+
150
+ def _tool(self, cid: str) -> Tool:
151
+ m = _discover.info(cid)
152
+ if not m or not m.get("inputs"):
153
+ raise ValueError(
154
+ f"Tool discovery failed for {cid}: Invalid or missing Wrekenfile schema"
155
+ )
156
+
157
+ name = _make_alias(cid, self._name_to_cid)
158
+
159
+ self._name_to_cid[name] = cid
160
+ raw_inputs = m.get("inputs")
161
+ self._cid_to_inputs[cid] = raw_inputs
162
+ return Tool(
163
+ canonical_id=cid,
164
+ name=name,
165
+ description=m.get("summary") or m.get("description") or cid,
166
+ input_schema=_schema.simplify(raw_inputs),
167
+ execute=lambda args, _c=cid, _inputs=raw_inputs: self.execute(
168
+ _c, args, _raw_inputs=_inputs
169
+ ),
170
+ )
171
+
172
+ def _ids(self, toolkits, tools, search) -> list[str]:
173
+ # Implementation for Phase 1: resolve against CLI local state
174
+ if tools:
175
+ return tools
176
+ if search:
177
+ # Mirror the TS runtime: resolve a natural-language search to
178
+ # canonical IDs via the CLI's discover/search.
179
+ return [
180
+ t["canonical_id"]
181
+ for t in _discover.search(search)
182
+ if t.get("canonical_id")
183
+ ]
184
+ if toolkits:
185
+ # Resolve toolkits against local tooling.json instead of global search
186
+ res = _manage.list_tools("tooling")
187
+ found = []
188
+ for m in res.get("methods", []):
189
+ integration = m.get("integration", "")
190
+ cid = m.get("canonical_id")
191
+ if not cid:
192
+ continue
193
+ for tk in toolkits:
194
+ if _toolkit_matches(tk, integration):
195
+ found.append(cid)
196
+ break
197
+ return found
198
+ return []
199
+
200
+
201
+ class Swytchcode:
202
+ def __init__(self, provider: Optional[Provider] = None):
203
+ self.provider = provider
204
+ self.tools = _Tools(self)
205
+
206
+ def handle_tool_calls(
207
+ self, response: Any, timeout: float | None = None
208
+ ) -> list[dict]:
209
+ """Helper to execute tools for non-agentic APIs like Anthropic."""
210
+ results = []
211
+ for block in getattr(response, "content", []):
212
+ if getattr(block, "type", "") == "tool_use":
213
+ # Reverse the sanitized name via the map built in _tool(). A plain
214
+ # "_"->"." replace would corrupt canonical IDs whose segments
215
+ # contain underscores (e.g. stripe.create_payment).
216
+ cid = self.tools._name_to_cid.get(block.name) or block.name.replace(
217
+ "_", "."
218
+ )
219
+ raw_inputs = self.tools._cid_to_inputs.get(cid, {})
220
+ # Isolate failures per block so one failing tool doesn't drop the
221
+ # results for the other tool_use blocks in the same turn.
222
+ try:
223
+ content = str(
224
+ self.tools.execute(
225
+ cid,
226
+ getattr(block, "input", {}),
227
+ _raw_inputs=raw_inputs,
228
+ timeout=timeout,
229
+ )
230
+ )
231
+ is_error = False
232
+ except Exception as e:
233
+ content = f"Error executing {cid}: {e}"
234
+ is_error = True
235
+ results.append(
236
+ {
237
+ "type": "tool_result",
238
+ "tool_use_id": block.id,
239
+ "content": content,
240
+ "is_error": is_error,
241
+ }
242
+ )
243
+ return results
@@ -59,6 +59,7 @@ def exec_( # noqa: A001 - shadowing intentional for API consistency with JS/Go
59
59
  raw: bool = False,
60
60
  dry_run: bool = False,
61
61
  allow_raw: bool = False,
62
+ timeout: float | None = None,
62
63
  ) -> Any:
63
64
  """
64
65
  Run swytchcode exec <canonical_id> with optional JSON args on stdin.
@@ -80,6 +81,7 @@ def exec_( # noqa: A001 - shadowing intentional for API consistency with JS/Go
80
81
  raw: If True, use --raw and return stdout as a string.
81
82
  dry_run: If True, pass --dry-run; request details are output instead of calling the server.
82
83
  allow_raw: If True, pass --allow-raw; required for executing raw methods (kernel default is disabled).
84
+ timeout: Optional timeout in seconds for the subprocess.
83
85
 
84
86
  Returns:
85
87
  Parsed result (any) or raw string when raw is True.
@@ -114,8 +116,12 @@ def exec_( # noqa: A001 - shadowing intentional for API consistency with JS/Go
114
116
  capture_output=True,
115
117
  cwd=cwd or os.getcwd(),
116
118
  env=run_env,
117
- timeout=None,
119
+ timeout=timeout,
118
120
  )
121
+ except subprocess.TimeoutExpired as e:
122
+ raise SwytchcodeError(
123
+ f"swytchcode exec timed out after {timeout} seconds", e
124
+ ) from e
119
125
  except FileNotFoundError as e:
120
126
  hint = (
121
127
  f"using binary at {bin_path!r}"
@@ -0,0 +1,27 @@
1
+ """Providers for integrating Swytchcode tools into various AI frameworks."""
2
+
3
+ __all__ = [
4
+ "AnthropicProvider",
5
+ "CrewAIProvider",
6
+ "LangGraphProvider",
7
+ "OpenAIAgentsProvider",
8
+ "VercelProvider",
9
+ ]
10
+
11
+
12
+ def __getattr__(name):
13
+ """Lazy provider imports for backward compatibility."""
14
+ _provider_map = {
15
+ "AnthropicProvider": "anthropic",
16
+ "OpenAIAgentsProvider": "openai_agents",
17
+ "VercelProvider": "vercel",
18
+ "LangGraphProvider": "langgraph",
19
+ "CrewAIProvider": "crewai",
20
+ }
21
+ if name in _provider_map:
22
+ module_name = _provider_map[name]
23
+ import importlib
24
+
25
+ mod = importlib.import_module(f".{module_name}", __package__)
26
+ return getattr(mod, name)
27
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -6,7 +6,7 @@ the Tool itself via its `execute` closure (which runs the Swytchcode CLI): agent
6
6
  frameworks (OpenAI Agents, Vercel, LangGraph, CrewAI) invoke that closure directly
7
7
  during their own loop, while non-agentic APIs (Anthropic) run it through
8
8
  `Swytchcode.handle_tool_calls`. Note this is deliberately NOT Composio's two-class
9
- agentic/non-agentic split — execution lives on the tool, not an injected function.
9
+ agentic/non-agentic split - execution lives on the tool, not an injected function.
10
10
  """
11
11
 
12
12
  from __future__ import annotations
@@ -22,7 +22,7 @@ class OpenAIAgentsProvider(Provider):
22
22
  return await asyncio.to_thread(tool.execute, kwargs)
23
23
 
24
24
  return FunctionTool(
25
- name=str(tool.name).replace(".", "_"),
25
+ name=str(tool.name),
26
26
  description=str(tool.description),
27
27
  params_json_schema=tool.input_schema,
28
28
  on_invoke_tool=_on_invoke,
@@ -15,10 +15,10 @@ class VercelProvider(Provider):
15
15
 
16
16
  # The handler MUST be synchronous. ai_sdk's sync generate_text() calls the
17
17
  # tool handler without awaiting it, so an async handler is silently never
18
- # awaited — the tool never runs and the model hallucinates success. A sync
18
+ # awaited - the tool never runs and the model hallucinates success. A sync
19
19
  # handler returns a plain value that works in generate_text AND is fine for
20
20
  # the async Tool.run path (which only awaits awaitables). Called as
21
- # handler(**kwargs) — arguments by field name, never a positional dict.
21
+ # handler(**kwargs) - arguments by field name, never a positional dict.
22
22
  def _execute(**kwargs):
23
23
  return tool.execute(kwargs)
24
24
 
@@ -36,15 +36,18 @@ def simplify(inputs: Any) -> dict:
36
36
  props[name] = {"type": t, "description": spec.get("DESC", "")}
37
37
 
38
38
  req = spec.get("REQUIRED", False)
39
- is_required = req is True or (
40
- isinstance(req, str) and req.strip().lower() == "true"
39
+ loc = str(spec.get("LOCATION", spec.get("location", ""))).lower()
40
+ is_required = (
41
+ loc == "path"
42
+ or req is True
43
+ or (isinstance(req, str) and req.strip().lower() == "true")
41
44
  )
42
45
  if is_required:
43
46
  required.append(name)
44
47
 
45
48
  # Composio-style rule: expose ALL fields to the model and list only the
46
49
  # truly-required ones in `required`. A required-only approach hid optional
47
- # fields — which left all-optional tools (e.g. Stripe) with an empty schema
50
+ # fields - which left all-optional tools (e.g. Stripe) with an empty schema
48
51
  # so the model called them with no arguments, and blinded the model to
49
52
  # optional fields on tools that do have some required ones.
50
53
  return {"type": "object", "properties": props, "required": required}
@@ -61,12 +64,13 @@ def simplify(inputs: Any) -> dict:
61
64
  # original required list only for the `required` key so optional/nested
62
65
  # fields stay optional instead of being dropped or forced required.
63
66
  for name, spec in props.items():
64
- if (
65
- isinstance(spec, dict)
66
- and spec.get("type") == "object"
67
- and "properties" in spec
68
- ):
69
- spec = simplify(spec) # recurse into nested objects
67
+ if isinstance(spec, dict):
68
+ loc = str(spec.get("LOCATION", spec.get("location", ""))).lower()
69
+ if loc == "path" and name not in required:
70
+ required.append(name)
71
+
72
+ if spec.get("type") == "object" and "properties" in spec:
73
+ spec = simplify(spec) # recurse into nested objects
70
74
  keep[name] = spec
71
75
 
72
76
  return {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swytchcode-runtime
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Thin runtime wrapper around the Swytchcode CLI
5
5
  Author: Swytchcode
6
6
  License: MIT
@@ -147,39 +147,74 @@ def create_account(input: dict) -> any:
147
147
 
148
148
  ## Agentic workflows (framework integrations)
149
149
 
150
- On top of `exec`, the runtime exposes a small agentic surface that turns Swytchcode tools
151
- into the native tool objects each agent framework expects. The Swytchcode part is the same
152
- two lines regardless of framework:
150
+ For full, production-ready examples across all major frameworks, check out the [Swytchcode Examples Repository](https://github.com/swytchcodehq/swytchcode-examples).
153
151
 
152
+ On top of `exec`, the runtime exposes a small agentic surface that turns Swytchcode tools into the native tool objects each agent framework expects.
153
+
154
+ ### Quickstart: Anthropic SDK
155
+
156
+ Here is a clean example of building a simple agent using the Anthropic SDK. We use `python-dotenv` to load environment variables (like `ANTHROPIC_API_KEY`).
157
+
158
+ **Installation:**
159
+ ```bash
160
+ pip install swytchcode-runtime anthropic python-dotenv
161
+ ```
162
+ *(Note: You only need to install the SDK for the framework you are actually using. You **do not** need to install `openai-agents` or `langchain` if you are only using Anthropic. The `swytchcode-runtime` isolates these dependencies via lazy loading.)*
163
+
164
+ **Example:**
154
165
  ```python
166
+ import os
167
+ from dotenv import load_dotenv
168
+ import anthropic
155
169
  from swytchcode_runtime import Swytchcode
156
170
  from swytchcode_runtime.providers.anthropic import AnthropicProvider
157
171
 
158
- swx = Swytchcode(provider=AnthropicProvider())
159
- tools = swx.tools.get(toolkits=["stripe"]) # framework-native tools, full param schemas
172
+ load_dotenv() # Loads .env automatically
173
+
174
+ def run_agent():
175
+ client = anthropic.Anthropic()
176
+
177
+ # 1. Initialize Swytchcode with the Anthropic provider
178
+ swx = Swytchcode(provider=AnthropicProvider())
179
+
180
+ # 2. Fetch the tools you want your agent to use (e.g., Stripe tools)
181
+ tools = swx.tools.get(toolkits=["stripe"])
182
+
183
+ # 3. Pass them to Claude
184
+ response = client.messages.create(
185
+ model="claude-3-5-sonnet-latest",
186
+ max_tokens=1024,
187
+ tools=tools,
188
+ messages=[{"role": "user", "content": "Refund charge ch_123 for $20."}],
189
+ )
190
+
191
+ print(response)
192
+
193
+ if __name__ == "__main__":
194
+ run_agent()
160
195
  ```
161
196
 
162
- ### Selecting tools — `swx.tools.get(...)`
197
+ ### Selecting tools - `swx.tools.get(...)`
163
198
 
164
199
  Pass exactly one selector; IDs resolve against your local Swytchcode state and remote search:
165
200
 
166
- - `toolkits=["stripe"]` — every enabled tool whose integration matches a toolkit.
167
- - `tools=["charges.charge.create"]` — explicit canonical IDs.
168
- - `search="refund a charge"` — natural-language discovery (via `swytchcode discover`).
201
+ - `toolkits=["stripe"]` - every enabled tool whose integration matches a toolkit.
202
+ - `tools=["charges.charge.create"]` - explicit canonical IDs.
203
+ - `search="refund a charge"` - natural-language discovery (via `swytchcode discover`).
169
204
 
170
- Each returned tool carries a **full input schema** — every field is surfaced to the model, with
171
- only the truly-required ones marked `required` — and an `execute` callback that runs `swytchcode
205
+ Each returned tool carries a **full input schema** - every field is surfaced to the model, with
206
+ only the truly-required ones marked `required` - and an `execute` callback that runs `swytchcode
172
207
  exec` for you (empty optional values are stripped before the call so APIs like Stripe don't reject them).
173
208
 
174
209
  ### Supported providers
175
210
 
176
211
  | Framework | Import | Who runs the tool loop |
177
212
  |-----------|--------|------------------------|
178
- | Anthropic Claude | `providers.anthropic.AnthropicProvider` | you (Messages API + `swx.handle_tool_calls`) |
179
- | OpenAI Agents SDK | `providers.openai_agents.OpenAIAgentsProvider` | the SDK |
180
- | Vercel AI SDK | `providers.vercel.VercelProvider` | the SDK |
181
- | LangGraph | `providers.langgraph.LangGraphProvider` | the prebuilt agent |
182
- | CrewAI | `providers.crewai.CrewAIProvider` | the crew |
213
+ | Anthropic Claude | `from swytchcode_runtime.providers.anthropic import AnthropicProvider` | you (Messages API + `swx.handle_tool_calls`) |
214
+ | OpenAI Agents SDK | `from swytchcode_runtime.providers.openai_agents import OpenAIAgentsProvider` | the SDK |
215
+ | Vercel AI SDK | `from swytchcode_runtime.providers.vercel import VercelProvider` | the SDK |
216
+ | LangGraph | `from swytchcode_runtime.providers.langgraph import LangGraphProvider` | the prebuilt agent |
217
+ | CrewAI | `from swytchcode_runtime.providers.crewai import CrewAIProvider` | the crew |
183
218
 
184
219
  ### Non-agentic APIs (Anthropic Messages)
185
220
 
@@ -19,4 +19,6 @@ swytchcode_runtime/providers/base.py
19
19
  swytchcode_runtime/providers/crewai.py
20
20
  swytchcode_runtime/providers/langgraph.py
21
21
  swytchcode_runtime/providers/openai_agents.py
22
- swytchcode_runtime/providers/vercel.py
22
+ swytchcode_runtime/providers/vercel.py
23
+ tests/test_alias.py
24
+ tests/test_schema_routing.py
@@ -0,0 +1,42 @@
1
+ import unittest
2
+ from swytchcode_runtime.client import Swytchcode, _make_alias
3
+ from unittest.mock import patch
4
+
5
+
6
+ class TestAlias(unittest.TestCase):
7
+ @patch("swytchcode_runtime.discover.info")
8
+ @patch("swytchcode_runtime.discover.search")
9
+ def test_deterministic_alias(self, mock_search, mock_info):
10
+ long_id = "google_workspace_admin_directory_users_aliases_insert_extra_padding_to_exceed_limit"
11
+ mock_search.return_value = [{"canonical_id": long_id}]
12
+ mock_info.return_value = {
13
+ "inputs": {"email": {"TYPE": "STRING"}},
14
+ "summary": "Test tool",
15
+ }
16
+
17
+ client = Swytchcode()
18
+ tools = client.tools.get(search="test")
19
+ self.assertEqual(len(tools), 1)
20
+ alias = tools[0].name
21
+
22
+ self.assertLessEqual(len(alias), 64)
23
+
24
+ # Test determinism
25
+ client2 = Swytchcode()
26
+ tools2 = client2.tools.get(search="test")
27
+ self.assertEqual(tools2[0].name, alias)
28
+
29
+ # Test collision
30
+ taken = {}
31
+ alias1 = _make_alias("a.b", taken)
32
+ taken[alias1] = "a.b"
33
+ alias2 = _make_alias("a_b", taken) # Collides on sanitize
34
+
35
+ self.assertNotEqual(alias1, alias2)
36
+
37
+ import re
38
+ self.assertTrue(re.search(r"_[0-9a-f]{6}$", alias2) or re.search(r"_[0-9a-f]{6}$", alias1))
39
+
40
+
41
+ if __name__ == "__main__":
42
+ unittest.main()
@@ -0,0 +1,62 @@
1
+ import unittest
2
+ from swytchcode_runtime.schema import simplify
3
+
4
+
5
+ class TestSchemaRouting(unittest.TestCase):
6
+ def test_path_params_required(self):
7
+ raw_schema = [
8
+ {"userId": {"TYPE": "STRING", "LOCATION": "path", "DESC": "The user ID"}},
9
+ {"amount": {"TYPE": "INT", "LOCATION": "query"}},
10
+ ]
11
+
12
+ simplified = simplify(raw_schema)
13
+
14
+ # Path parameters should be required
15
+ self.assertIn("userId", simplified["required"])
16
+ self.assertEqual(simplified["properties"]["userId"]["type"], "string")
17
+ self.assertEqual(simplified["properties"]["amount"]["type"], "integer")
18
+
19
+ def test_json_schema_path_params_required(self):
20
+ raw_schema = {
21
+ "type": "object",
22
+ "properties": {
23
+ "owner": {"type": "string", "location": "path"},
24
+ "repo": {"type": "string", "location": "path"},
25
+ "title": {"type": "string", "location": "body"},
26
+ },
27
+ }
28
+ simplified = simplify(raw_schema)
29
+ self.assertIn("owner", simplified["required"])
30
+ self.assertIn("repo", simplified["required"])
31
+ self.assertNotIn("title", simplified["required"])
32
+
33
+ def test_split_by_location_json_schema(self):
34
+ from swytchcode_runtime.client import _split_by_location
35
+
36
+ raw_schema = {
37
+ "type": "object",
38
+ "properties": {
39
+ "owner": {"type": "string", "location": "path"},
40
+ "title": {"type": "string", "location": "body"},
41
+ },
42
+ }
43
+ args = {"owner": "swytchcode", "title": "hello"}
44
+ result = _split_by_location(raw_schema, args)
45
+ self.assertEqual(result.get("params", {}).get("owner"), "swytchcode")
46
+ self.assertEqual(result.get("body", {}).get("title"), "hello")
47
+
48
+ def test_split_by_location_array_wreken_shape(self):
49
+ from swytchcode_runtime.client import _split_by_location
50
+
51
+ raw_schema = [
52
+ {"owner": {"TYPE": "STRING", "LOCATION": "path"}},
53
+ {"title": {"TYPE": "STRING", "LOCATION": "body"}},
54
+ ]
55
+ args = {"owner": "swytchcode", "title": "hello"}
56
+ result = _split_by_location(raw_schema, args)
57
+ self.assertEqual(result.get("params", {}).get("owner"), "swytchcode")
58
+ self.assertEqual(result.get("body", {}).get("title"), "hello")
59
+
60
+
61
+ if __name__ == "__main__":
62
+ unittest.main()
@@ -1,24 +0,0 @@
1
- """Thin runtime wrapper around the Swytchcode CLI."""
2
-
3
- from .errors import SwytchcodeError, is_swytchcode_error
4
- from .exec import exec_ as exec
5
- from .client import Swytchcode
6
- from .providers import (
7
- AnthropicProvider,
8
- OpenAIAgentsProvider,
9
- VercelProvider,
10
- LangGraphProvider,
11
- CrewAIProvider,
12
- )
13
-
14
- __all__ = [
15
- "exec",
16
- "Swytchcode",
17
- "SwytchcodeError",
18
- "is_swytchcode_error",
19
- "AnthropicProvider",
20
- "OpenAIAgentsProvider",
21
- "VercelProvider",
22
- "LangGraphProvider",
23
- "CrewAIProvider",
24
- ]
@@ -1,133 +0,0 @@
1
- """High-level agentic client on top of the existing exec()."""
2
-
3
- from __future__ import annotations
4
- from typing import Any, Optional
5
-
6
- from . import discover as _discover, schema as _schema, manage as _manage
7
- from .exec import exec_ as _exec
8
- from .providers.base import Provider, Tool
9
-
10
-
11
- def _strip_empty(obj: Any) -> Any:
12
- """Recursively drop keys whose value is None or an empty string ("").
13
-
14
- With 'expose all fields', agents often fill unused optional fields with "",
15
- which APIs like Stripe reject ("empty values are an attempt to unset").
16
- Only None and "" are dropped — 0, False, [], {} are preserved as meaningful.
17
- """
18
- if isinstance(obj, dict):
19
- out = {}
20
- for k, v in obj.items():
21
- if v is None or (isinstance(v, str) and v == ""):
22
- continue
23
- out[k] = _strip_empty(v)
24
- return out
25
- if isinstance(obj, list):
26
- return [_strip_empty(v) for v in obj]
27
- return obj
28
-
29
-
30
- class _Tools:
31
- def __init__(self, client: "Swytchcode"):
32
- self._c = client
33
- # Maps a sanitized tool name (dots -> underscores) back to its canonical
34
- # ID, populated as tools are built. Used to reverse names in
35
- # handle_tool_calls without a lossy "_"->"." string replace.
36
- self._name_to_cid: dict[str, str] = {}
37
-
38
- def get(self, *, toolkits=None, tools=None, search=None):
39
- neutral = [self._tool(cid) for cid in self._ids(toolkits, tools, search)]
40
- p = self._c.provider
41
- return p.format_tools(neutral) if p else neutral
42
-
43
- def execute(self, canonical_id: str, args: dict, **options) -> Any:
44
- # If args are flat (no body/params top-level keys), wrap them in body
45
- # as expected by the Swytchcode CLI kernel (like in run-workflow.js)
46
- if "body" not in args and "params" not in args:
47
- args = {"body": args}
48
- # Drop empty optional fields (None/"") from body & params so values an
49
- # agent over-filled don't reach the API (e.g. Stripe rejects customer="").
50
- args = dict(args)
51
- if isinstance(args.get("body"), (dict, list)):
52
- args["body"] = _strip_empty(args["body"])
53
- if isinstance(args.get("params"), (dict, list)):
54
- args["params"] = _strip_empty(args["params"])
55
- # Forward exec options (dry_run, raw, allow_raw, cwd, env) to the CLI.
56
- return _exec(canonical_id, args, **options)
57
-
58
- def _tool(self, cid: str) -> Tool:
59
- m = _discover.info(cid)
60
- name = cid.replace(".", "_")
61
- self._name_to_cid[name] = cid
62
- return Tool(
63
- canonical_id=cid,
64
- name=name,
65
- description=m.get("summary") or m.get("description") or cid,
66
- input_schema=_schema.simplify(m.get("inputs")),
67
- execute=lambda args, _c=cid: self.execute(_c, args),
68
- )
69
-
70
- def _ids(self, toolkits, tools, search) -> list[str]:
71
- # Implementation for Phase 1: resolve against CLI local state
72
- if tools:
73
- return tools
74
- if search:
75
- # Mirror the TS runtime: resolve a natural-language search to
76
- # canonical IDs via the CLI's discover/search.
77
- return [
78
- t["canonical_id"]
79
- for t in _discover.search(search)
80
- if t.get("canonical_id")
81
- ]
82
- if toolkits:
83
- # Resolve toolkits against local tooling.json instead of global search
84
- res = _manage.list_tools("tooling")
85
- found = []
86
- for m in res.get("methods", []):
87
- integration = m.get("integration", "")
88
- cid = m.get("canonical_id")
89
- if not cid:
90
- continue
91
- for tk in toolkits:
92
- # integration is "project.library@version"; match the project
93
- # segment exactly, not a loose substring (so "hub" != "github").
94
- if tk == integration or integration.startswith(f"{tk}."):
95
- found.append(cid)
96
- break
97
- return found
98
- return []
99
-
100
-
101
- class Swytchcode:
102
- def __init__(self, provider: Optional[Provider] = None):
103
- self.provider = provider
104
- self.tools = _Tools(self)
105
-
106
- def handle_tool_calls(self, response: Any) -> list[dict]:
107
- """Helper to execute tools for non-agentic APIs like Anthropic."""
108
- results = []
109
- for block in getattr(response, "content", []):
110
- if getattr(block, "type", "") == "tool_use":
111
- # Reverse the sanitized name via the map built in _tool(). A plain
112
- # "_"->"." replace would corrupt canonical IDs whose segments
113
- # contain underscores (e.g. stripe.create_payment).
114
- cid = self.tools._name_to_cid.get(block.name) or block.name.replace(
115
- "_", "."
116
- )
117
- # Isolate failures per block so one failing tool doesn't drop the
118
- # results for the other tool_use blocks in the same turn.
119
- try:
120
- content = str(self.tools.execute(cid, getattr(block, "input", {})))
121
- is_error = False
122
- except Exception as e:
123
- content = f"Error executing {cid}: {e}"
124
- is_error = True
125
- results.append(
126
- {
127
- "type": "tool_result",
128
- "tool_use_id": block.id,
129
- "content": content,
130
- "is_error": is_error,
131
- }
132
- )
133
- return results
@@ -1,15 +0,0 @@
1
- """Providers for integrating Swytchcode tools into various AI frameworks."""
2
-
3
- from .anthropic import AnthropicProvider
4
- from .crewai import CrewAIProvider
5
- from .langgraph import LangGraphProvider
6
- from .openai_agents import OpenAIAgentsProvider
7
- from .vercel import VercelProvider
8
-
9
- __all__ = [
10
- "AnthropicProvider",
11
- "CrewAIProvider",
12
- "LangGraphProvider",
13
- "OpenAIAgentsProvider",
14
- "VercelProvider",
15
- ]