Skip to content

Commit 78d17be

Browse files
masnwilliamsclaude
andcommitted
Add unified CUA template with multi-provider fallback
Adds a new 'cua' template (TypeScript + Python) that supports Anthropic, OpenAI, and Gemini CUA providers behind a clean abstraction layer. Includes automatic fallback on provider-level errors (rate limits, API errors) while preserving task-level error behavior. Config-driven via CUA_PROVIDER and CUA_FALLBACK_PROVIDERS env vars, with per-request provider override support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6d54518 commit 78d17be

22 files changed

Lines changed: 2491 additions & 0 deletions

pkg/create/templates.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const (
1919
TemplateOpenAGIComputerUse = "openagi-computer-use"
2020
TemplateClaudeAgentSDK = "claude-agent-sdk"
2121
TemplateYutoriComputerUse = "yutori"
22+
TemplateCUA = "cua"
2223
)
2324

2425
type TemplateInfo struct {
@@ -90,6 +91,11 @@ var Templates = map[string]TemplateInfo{
9091
Description: "Implements a Yutori n1 computer use agent",
9192
Languages: []string{LanguageTypeScript, LanguagePython},
9293
},
94+
TemplateCUA: {
95+
Name: "Unified CUA",
96+
Description: "Multi-provider CUA with automatic fallback (Anthropic, OpenAI, Gemini)",
97+
Languages: []string{LanguageTypeScript, LanguagePython},
98+
},
9399
}
94100

95101
// GetSupportedTemplatesForLanguage returns a list of all supported template names for a given language
@@ -213,6 +219,11 @@ var Commands = map[string]map[string]DeployConfig{
213219
NeedsEnvFile: true,
214220
InvokeCommand: `kernel invoke ts-yutori-cua cua-task --payload '{"query": "Navigate to https://example.com and describe the page"}'`,
215221
},
222+
TemplateCUA: {
223+
EntryPoint: "index.ts",
224+
NeedsEnvFile: true,
225+
InvokeCommand: `kernel invoke ts-cua cua-task --payload '{"query": "Navigate to https://example.com and describe the page"}'`,
226+
},
216227
},
217228
LanguagePython: {
218229
TemplateSampleApp: {
@@ -260,6 +271,11 @@ var Commands = map[string]map[string]DeployConfig{
260271
NeedsEnvFile: true,
261272
InvokeCommand: `kernel invoke python-yutori-cua cua-task --payload '{"query": "Navigate to https://example.com and describe the page"}'`,
262273
},
274+
TemplateCUA: {
275+
EntryPoint: "main.py",
276+
NeedsEnvFile: true,
277+
InvokeCommand: `kernel invoke python-cua cua-task --payload '{"query": "Navigate to https://example.com and describe the page"}'`,
278+
},
263279
},
264280
}
265281

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Primary provider: anthropic, openai, or gemini
2+
CUA_PROVIDER=anthropic
3+
4+
# Fallback providers (comma-separated, tried in order on provider errors)
5+
# CUA_FALLBACK_PROVIDERS=openai,gemini
6+
7+
# Provider API keys (set the ones you need)
8+
ANTHROPIC_API_KEY=your_anthropic_api_key_here
9+
# OPENAI_API_KEY=your_openai_api_key_here
10+
# GOOGLE_API_KEY=your_google_api_key_here

pkg/templates/python/cua/README.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Kernel Unified CUA Template (Python)
2+
3+
A unified Computer Use Agent (CUA) template that supports multiple providers with automatic fallback.
4+
5+
## Supported Providers
6+
7+
| Provider | Model | Env Var |
8+
|----------|-------|---------|
9+
| Anthropic | `claude-sonnet-4-6` | `ANTHROPIC_API_KEY` |
10+
| OpenAI | `gpt-5.4` | `OPENAI_API_KEY` |
11+
| Gemini | `gemini-2.5-computer-use-preview-10-2025` | `GOOGLE_API_KEY` |
12+
13+
## Setup
14+
15+
1. Get your API keys:
16+
- **Kernel**: [dashboard.onkernel.com](https://dashboard.onkernel.com)
17+
- **Anthropic**: [console.anthropic.com](https://console.anthropic.com)
18+
- **OpenAI**: [platform.openai.com](https://platform.openai.com)
19+
- **Google**: [aistudio.google.com](https://aistudio.google.com)
20+
21+
2. Configure and deploy:
22+
```bash
23+
kernel login
24+
cp .env.example .env # Add your API keys and configure providers
25+
kernel deploy main.py --env-file .env
26+
```
27+
28+
## Configuration
29+
30+
Set these environment variables in your `.env` file:
31+
32+
```bash
33+
# Primary provider (default: anthropic)
34+
CUA_PROVIDER=anthropic
35+
36+
# Fallback providers, tried in order on provider errors (optional)
37+
CUA_FALLBACK_PROVIDERS=openai,gemini
38+
39+
# API keys for each provider you want to use
40+
ANTHROPIC_API_KEY=sk-ant-...
41+
OPENAI_API_KEY=sk-...
42+
GOOGLE_API_KEY=AI...
43+
```
44+
45+
## Usage
46+
47+
```bash
48+
# Use default provider
49+
kernel invoke python-cua cua-task --payload '{"query": "Navigate to https://example.com and describe the page"}'
50+
51+
# Override provider per-request
52+
kernel invoke python-cua cua-task --payload '{"query": "Search for kernel.sh", "provider": "openai"}'
53+
54+
# With replay recording
55+
kernel invoke python-cua cua-task --payload '{"query": "Navigate to https://example.com", "record_replay": true}'
56+
```
57+
58+
## How Fallback Works
59+
60+
The fallback mechanism triggers on **provider-level errors** only:
61+
- Rate limits (429)
62+
- Server errors (500, 502, 503)
63+
- Authentication failures
64+
- Network errors
65+
66+
It does **not** fall back on task-level failures.
67+
68+
## Architecture
69+
70+
```
71+
main.py Entry point, Kernel app registration
72+
session.py Browser lifecycle management (shared)
73+
tools.py Common action types + Kernel API executor (shared)
74+
providers/
75+
__init__.py Provider factory + fallback logic
76+
anthropic.py Anthropic Claude computer use adapter
77+
openai_provider.py OpenAI CUA adapter
78+
gemini.py Google Gemini computer use adapter
79+
```
80+
81+
## Resources
82+
83+
- [Kernel Documentation](https://www.kernel.sh/docs/quickstart)
84+
- [Anthropic Computer Use](https://docs.anthropic.com/en/docs/build-with-claude/computer-use)
85+
- [OpenAI Computer Use](https://platform.openai.com/docs/guides/computer-use)
86+
- [Gemini Computer Use](https://ai.google.dev/gemini-api/docs/computer-use)

pkg/templates/python/cua/main.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""
2+
Unified CUA (Computer Use Agent) template.
3+
4+
Supports Anthropic, OpenAI, and Gemini providers with automatic fallback.
5+
Configure via environment variables:
6+
CUA_PROVIDER - Primary provider: anthropic, openai, or gemini (default: anthropic)
7+
CUA_FALLBACK_PROVIDERS - Comma-separated fallback order (e.g. "openai,gemini")
8+
ANTHROPIC_API_KEY - Required if using Anthropic
9+
OPENAI_API_KEY - Required if using OpenAI
10+
GOOGLE_API_KEY - Required if using Gemini
11+
"""
12+
13+
import os
14+
from typing import NotRequired, Optional, TypedDict
15+
16+
import kernel
17+
from session import KernelBrowserSession
18+
from tools import KernelExecutor
19+
from providers import run_with_fallback, ProviderConfig
20+
21+
# Parse provider configuration
22+
PRIMARY_PROVIDER = os.getenv("CUA_PROVIDER", "anthropic")
23+
FALLBACK_PROVIDERS = [
24+
p.strip() for p in os.getenv("CUA_FALLBACK_PROVIDERS", "").split(",") if p.strip()
25+
]
26+
PROVIDER_CHAIN = [PRIMARY_PROVIDER] + FALLBACK_PROVIDERS
27+
28+
API_KEY_MAP = {
29+
"anthropic": "ANTHROPIC_API_KEY",
30+
"openai": "OPENAI_API_KEY",
31+
"gemini": "GOOGLE_API_KEY",
32+
}
33+
34+
configured = [p for p in PROVIDER_CHAIN if os.getenv(API_KEY_MAP.get(p, ""))]
35+
if not configured:
36+
keys = [API_KEY_MAP.get(p, p) for p in PROVIDER_CHAIN]
37+
raise ValueError(
38+
f"No API keys found for configured providers {PROVIDER_CHAIN}. "
39+
f"Set at least one of: {', '.join(keys)}"
40+
)
41+
42+
43+
class CuaInput(TypedDict):
44+
query: str
45+
provider: NotRequired[str]
46+
model: NotRequired[str]
47+
record_replay: NotRequired[bool]
48+
49+
50+
class CuaOutput(TypedDict):
51+
result: str
52+
provider: str
53+
replay_url: Optional[str]
54+
55+
56+
app = kernel.App("python-cua")
57+
58+
59+
@app.action("cua-task")
60+
async def cua_task(
61+
ctx: kernel.KernelContext,
62+
payload: CuaInput,
63+
) -> CuaOutput:
64+
if not payload or not payload.get("query"):
65+
raise ValueError("Query is required")
66+
67+
# Allow per-request provider override
68+
if payload.get("provider"):
69+
provider_chain = [payload["provider"]] + [p for p in PROVIDER_CHAIN if p != payload["provider"]]
70+
else:
71+
provider_chain = PROVIDER_CHAIN
72+
73+
record_replay = payload.get("record_replay", False)
74+
75+
async with KernelBrowserSession(
76+
invocation_id=ctx.invocation_id,
77+
stealth=True,
78+
record_replay=record_replay,
79+
) as session:
80+
print("Kernel browser live view url:", session.live_view_url)
81+
82+
executor = KernelExecutor(session.kernel, session.session_id)
83+
84+
result = await run_with_fallback(
85+
provider_chain,
86+
ProviderConfig(
87+
query=payload["query"],
88+
model=payload.get("model"),
89+
viewport_width=session.viewport_width,
90+
viewport_height=session.viewport_height,
91+
),
92+
executor,
93+
)
94+
95+
return {
96+
"result": result.result,
97+
"provider": result.provider,
98+
"replay_url": session.replay_view_url,
99+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""
2+
Provider factory and fallback logic.
3+
4+
Creates provider instances and handles automatic fallback on provider errors
5+
(rate limits, API errors). Does NOT fall back on task-level failures.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from dataclasses import dataclass
11+
from typing import Optional, Protocol
12+
13+
from tools import KernelExecutor
14+
15+
16+
@dataclass
17+
class ProviderConfig:
18+
query: str
19+
model: Optional[str] = None
20+
api_key: Optional[str] = None
21+
viewport_width: int = 1280
22+
viewport_height: int = 800
23+
24+
25+
@dataclass
26+
class ProviderResult:
27+
result: str
28+
provider: str
29+
30+
31+
class CUAProvider(Protocol):
32+
name: str
33+
34+
async def run(self, config: ProviderConfig, executor: KernelExecutor) -> ProviderResult: ...
35+
36+
37+
def create_provider(name: str) -> CUAProvider:
38+
if name == "anthropic":
39+
from providers.anthropic import AnthropicProvider
40+
return AnthropicProvider()
41+
elif name == "openai":
42+
from providers.openai_provider import OpenAIProvider
43+
return OpenAIProvider()
44+
elif name == "gemini":
45+
from providers.gemini import GeminiProvider
46+
return GeminiProvider()
47+
else:
48+
raise ValueError(f"Unknown provider: {name}. Supported: anthropic, openai, gemini")
49+
50+
51+
# Errors that indicate a provider-level failure (should trigger fallback)
52+
_PROVIDER_ERROR_KEYWORDS = [
53+
"rate limit", "429", "503", "502", "500", "overloaded", "capacity",
54+
"api key", "authentication", "unauthorized", "forbidden", "quota",
55+
"timeout", "connection", "refused", "reset",
56+
]
57+
58+
59+
def _is_provider_error(error: Exception) -> bool:
60+
msg = str(error).lower()
61+
return any(kw in msg for kw in _PROVIDER_ERROR_KEYWORDS)
62+
63+
64+
async def run_with_fallback(
65+
providers: list[str],
66+
config: ProviderConfig,
67+
executor: KernelExecutor,
68+
) -> ProviderResult:
69+
"""
70+
Run a CUA task with automatic fallback across providers.
71+
72+
Tries the primary provider first. On provider-level errors (rate limits, API errors),
73+
falls back to the next provider. Does NOT fall back on task-level failures.
74+
"""
75+
errors: list[tuple[str, Exception]] = []
76+
77+
for provider_name in providers:
78+
provider = create_provider(provider_name)
79+
try:
80+
print(f"[cua] Trying provider: {provider_name}")
81+
result = await provider.run(config, executor)
82+
print(f"[cua] Provider {provider_name} succeeded")
83+
return result
84+
except Exception as e:
85+
errors.append((provider_name, e))
86+
print(f"[cua] Provider {provider_name} failed: {e}")
87+
88+
if not _is_provider_error(e):
89+
raise
90+
91+
if provider_name == providers[-1]:
92+
error_details = "\n".join(f" {name}: {err}" for name, err in errors)
93+
raise RuntimeError(f"All providers failed:\n{error_details}") from e
94+
95+
print("[cua] Falling back to next provider...")
96+
97+
raise RuntimeError("No providers configured")

0 commit comments

Comments
 (0)