# Agent Chat Source: https://docs.raysurfer.com/agent-chat Persistent agent workspaces that survive across conversations Give your AI agent a persistent workspace scoped per user and org. Files, state, and context are automatically saved and restored — no setup needed. ## How It Works 1. **You call `chat()`** with a query, user ID, and org ID 2. **Raysurfer restores the workspace** — all files from prior calls for that user+org are mounted automatically 3. **Claude executes the task** with full tool access (Read, Write, Bash, etc.) 4. **Raysurfer persists the workspace** — any files created or modified are saved for next time ## Usage ```python Python theme={null} from raysurfer import AsyncRaySurfer rs = AsyncRaySurfer() # First call — agent creates files from scratch response = await rs.chat( "Create hello.py with a greet(name) function", user="user-123", org="acme-corp", ) print(response.output) print(response.changed_files) # ["hello.py"] # Second call — agent sees hello.py already in its workspace response = await rs.chat( "Import greet from hello.py and create main.py", user="user-123", org="acme-corp", ) print(response.workspace_files) # ["hello.py", "main.py"] ``` ```typescript TypeScript theme={null} import { RaySurfer } from "raysurfer"; const rs = new RaySurfer(); const response = await rs.chat( "Create hello.ts with a greet(name) function", { user: "user-123", org: "acme-corp" }, ); console.log(response.output); console.log(response.changedFiles); const response2 = await rs.chat( "Import greet from hello.ts and create main.ts", { user: "user-123", org: "acme-corp" }, ); console.log(response2.workspaceFiles); ``` ```bash cURL theme={null} curl -X POST https://api.raysurfer.com/api/agent-chat \ -H "Authorization: Bearer $RAYSURFER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_query": "Create hello.py with a greet(name) function", "user_id": "user-123", "org_id": "acme-corp" }' ``` ## Parameters | Parameter | Type | Default | Description | | ----------- | ------ | ---------- | ---------------------------------------------- | | `query` | string | required | The task or question | | `user` | string | required | User identifier (scopes the workspace) | | `org` | string | required | Organization identifier (scopes the workspace) | | `model` | string | `"sonnet"` | Claude model to use | | `max_turns` | int | `8` | Maximum agent turns per request | ## Response ```python Python theme={null} response = await rs.chat("Create a Flask app", user="dev-1", org="startup") response.success # True response.output # "I created app.py with a Flask app..." response.changed_files # ["app.py"] response.workspace_files # ["app.py"] response.duration_ms # 4200 response.session_id # "sess_abc123" ``` ```typescript TypeScript theme={null} const response = await rs.chat("Create a Flask app", { user: "dev-1", org: "startup" }); response.success; // true response.output; // "I created app.py with a Flask app..." response.changedFiles; // ["app.py"] response.workspaceFiles; // ["app.py"] response.durationMs; // 4200 response.sessionId; // "sess_abc123" ``` ## Workspace Scoping Workspaces are scoped by `(org, user)`: * **Same user + same org** = same workspace (state persists) * **Different user or different org** = separate workspace Use this to give each of your end users their own persistent agent workspace. ## Multi-Turn Example ```python theme={null} from raysurfer import AsyncRaySurfer rs = AsyncRaySurfer() # Turn 1: Create a project await rs.chat("Create a Flask app with a /health endpoint", user="dev-1", org="startup") # Turn 2: Agent sees app.py from turn 1 await rs.chat("Add a /users endpoint that returns a list of users", user="dev-1", org="startup") # Turn 3: Agent sees both endpoints await rs.chat("Add tests for all endpoints", user="dev-1", org="startup") ``` Each call builds on the previous workspace — the agent sees all files from prior turns. ## Skill Document AI agents can read the full API reference at: ``` https://api.raysurfer.com/skill.md ``` Point your agent's system prompt or skill loader at this URL for self-serve integration. # Agent-Owned Repos Source: https://docs.raysurfer.com/agent-owned-repos How agents build up a library of proven code over time # Agent-Owned Repos ## What This Means An agent-owned repo is the idea that agents can build up a library of proven code over time, based on execution results rather than one-off code generation. Instead of generating from scratch every run, agents: 1. **Search** for proven code that already solved similar tasks 2. **Execute** the code in an isolated sandbox environment 3. **Score** the results — AI automatically evaluates code quality and promotes what works 4. **Build a library** — over time, the agent accumulates a growing collection of reliable code ## Quick Start Pass your chat history as Anthropic-typed messages. The agent handles caching, code reuse, and quality scoring automatically. ```python Python theme={null} from raysurfer import Agent agent = Agent() result = await agent.run( org_id="acme-corp", user_id="user_123", messages=[ {"role": "user", "content": "Generate a quarterly report from our sales data"}, ], ) ``` ```typescript TypeScript theme={null} import { Agent } from "raysurfer"; const agent = new Agent(); const result = await agent.run( [{ role: "user", content: "Generate a quarterly report from our sales data" }], { orgId: "acme-corp", userId: "user_123" }, ); ``` ## How Code Quality Improves Over Time Every time `agent.run()` processes a conversation, it tracks which cached code snippets were retrieved and used. AI automatically evaluates execution results and votes on all the code that contributed: * **Successful execution** → thumbs up on every cached snippet used → those snippets rank higher in future searches * **Failed execution** → thumbs down → those snippets get demoted and eventually stop surfacing This creates a closed loop: execution quality directly determines which code gets reused. Code that consistently produces good results rises to the top. Code that doesn't gets buried — all without any manual intervention. ``` Chat messages → search cached code → execute → produce result ↓ AI evaluates quality ↓ automatic vote on all code that contributed ↓ good code promoted, bad code demoted ``` New code generated during execution is also AI-voted at upload time, so it enters the library with an initial quality signal immediately. ## The Core Loop Under the hood, `agent.run()` wraps two API calls per conversation: 1. **`raysurfer.search()`** — find proven code that already solved similar tasks 2. **`raysurfer.upload()`** — store any new code generated for future reuse ``` Search proven code → Execute → Upload new code → AI scores quality ``` This makes agent behavior more stable over time. Agents start from high-signal code paths and only generate new code when necessary. ## Persistence The code library persists per organization. Every agent session within the same `org_id` shares the same pool of proven code. This means: * Code uploaded by one user's session is available to all users in the org * AI quality scoring affects the ranking for everyone * The library improves continuously without manual curation ## Sandbox Execution When agents execute code through Raysurfer, it runs in an isolated environment. This means: * Code runs safely without affecting your production systems * Execution results are captured and used for reputation scoring * Failed code is automatically excluded from future retrieval ## Recommended Rollout 1. Add Raysurfer to one high-frequency workflow first. 2. Track which snippets are repeatedly reused with positive scores. 3. Expand to additional workflows once retrieval quality is stable. Keep values parameterized in snippets. Hardcode only values you expect to be reused verbatim. Book a 15-minute call and we'll help you pick the right workflows to start with. Inspect publicly ranked snippets and quality signals that feed reuse decisions. Learn about proven code retrieval and reputation scoring. Add the same loop through Python/TypeScript SDKs, CLI, MCP, skill, or plugin. See real-world accuracy and speed improvements. # API Reference Source: https://docs.raysurfer.com/api-reference Complete reference for all publicly exposed SDK functions and types This page documents all publicly exposed functions and types in the Raysurfer SDK. Every type is defined down to primitive fields. *** ## High-Level Client ### RaysurferClient Drop-in replacement for Claude Agent SDK's `ClaudeSDKClient`. ```python Python theme={null} from raysurfer import RaysurferClient from claude_agent_sdk import ClaudeAgentOptions options = ClaudeAgentOptions( allowed_tools=["Read", "Write", "Bash"], system_prompt="You are a helpful assistant.", ) async with RaysurferClient(options) as client: await client.query("Your task here") async for msg in client.response(): print(msg) ``` ```typescript TypeScript theme={null} import { RaysurferClient } from "raysurfer"; const client = new RaysurferClient({ allowedTools: ["Read", "Write", "Bash"], systemPrompt: "You are a helpful assistant.", }); for await (const msg of client.query("Your task here")) { console.log(msg); } ``` #### Constructor Parameters **Python** — `RaysurferClient(options, workspace_id=None, debug=False)`: | Parameter | Type | Description | | -------------- | -------------------- | ----------------------------------------------------------- | | `options` | `ClaudeAgentOptions` | Standard Claude Agent SDK options | | `workspace_id` | `string` | Workspace ID for per-customer isolation (Pro or Enterprise) | | `debug` | `boolean` | Enable debug logging | **TypeScript** — `new RaysurferClient(options)` where options is a single object: | Parameter | Type | Description | | -------------- | ---------- | ----------------------------------------------------------- | | `allowedTools` | `string[]` | Allowed Claude tools | | `systemPrompt` | `string` | System prompt | | `model` | `string` | Claude model ID | | `workspaceId` | `string` | Workspace ID for per-customer isolation (Pro or Enterprise) | | `debug` | `boolean` | Enable debug logging | #### Methods | Method | Description | | --------------- | -------------------------------------------------------- | | `query(prompt)` | Send a query with automatic cache retrieval | | `response()` | Async generator yielding response messages (Python only) | *** ## Low-Level API Client ### RaySurfer / AsyncRaySurfer ```python Python theme={null} from raysurfer import RaySurfer, AsyncRaySurfer # Sync client client = RaySurfer(api_key="your_api_key") # Async client async with AsyncRaySurfer(api_key="your_api_key") as client: result = await client.search(task="Fetch GitHub repos") ``` ```typescript TypeScript theme={null} import { RaySurfer } from "raysurfer"; const client = new RaySurfer({ apiKey: "your_api_key" }); const result = await client.search({ task: "Fetch GitHub repos" }); ``` #### Constructor Options | Option | Type | Description | | ------------------------------------ | ------------------------- | ------------------------------------------------------------- | | `api_key` / `apiKey` | `string` | Your Raysurfer API key | | `base_url` / `baseUrl` | `string` | API base URL (optional) | | `timeout` | `number` | Request timeout in seconds/ms | | `organization_id` / `organizationId` | `string` | Organization ID for team/enterprise namespacing | | `workspace_id` / `workspaceId` | `string` | Workspace ID for per-customer namespacing (Pro or Enterprise) | | `snips_desired` / `snipsDesired` | `"company"` \| `"client"` | Scope of private snippets | | `public_snips` / `publicSnips` | `boolean` | Include community public snippets | *** ## search Search for cached code snippets by task description. ```python Python theme={null} result = client.search( task="Fetch GitHub trending repos", top_k=5, min_verdict_score=0.3, prefer_complete=True, ) for match in result.matches: print(f"{match.code_block.name}: {match.score}") print(f" File: {match.filename}") print(f" Source: {match.code_block.source[:80]}...") ``` ```typescript TypeScript theme={null} const result = await client.search({ task: "Fetch GitHub trending repos", topK: 5, minVerdictScore: 0.3, preferComplete: true, }); for (const match of result.matches) { console.log(`${match.codeBlock.name}: ${match.score}`); console.log(` File: ${match.filename}`); console.log(` Source: ${match.codeBlock.source.slice(0, 80)}...`); } ``` ### Parameters | Parameter | Type | Default | Description | | --------------------------------------------------- | --------- | -------- | ----------------------------------------------------------------------------------- | | `task` | `string` | required | Natural language task description (any length) | | `top_k` / `topK` | `number` | `5` | Maximum number of results | | `min_verdict_score` / `minVerdictScore` | `number` | `0.3` | Minimum quality score filter | | `prefer_complete` / `preferComplete` | `boolean` | `false` | Prefer longer/more complete implementations | | `input_schema` / `inputSchema` | `object` | `null` | Optional input schema filter | | `per_function_reputation` / `perFunctionReputation` | `boolean` | `false` | Inject per-function diagnostics into matched source and return `functions` metadata | ### Returns: [SearchResponse](#searchresponse) Queries can be any length, but shorter queries (under 200 characters) give the best accuracy and speed. Longer queries are automatically rephrased into multiple shorter searches and merged, which adds latency and may reduce precision. **LLM prompt augmentation**: The legacy convenience method `get_code_files()` / `getCodeFiles()` wraps `search()` and returns an `add_to_llm_prompt` string. Use `search()` directly for full control over results. *** ## upload Upload a code file from an execution for future reuse. ```python Python theme={null} from raysurfer.types import FileWritten file = FileWritten(path="fetch_repos.py", content="def fetch(): ...") # Let Raysurfer AI vote (default behavior) result = client.upload( task="Fetch GitHub trending repos", file_written=file, succeeded=True, execution_logs="Fetched 10 trending repos successfully", ) # Or provide your own vote (AI voting is automatically skipped) result = client.upload( task="Fetch GitHub trending repos", file_written=file, succeeded=True, user_vote=1, # 1 = thumbs up, -1 = thumbs down ) print(f"Stored {result.code_blocks_stored} code blocks") ``` ```typescript TypeScript theme={null} const file = { path: "fetch_repos.ts", content: "function fetch() { ... }" }; // Let Raysurfer AI vote (default behavior) const result = await client.upload({ task: "Fetch GitHub trending repos", fileWritten: file, succeeded: true, executionLogs: "Fetched 10 trending repos successfully", }); // Or provide your own vote (AI voting is automatically skipped) const result2 = await client.upload({ task: "Fetch GitHub trending repos", fileWritten: file, succeeded: true, userVote: 1, // 1 = thumbs up, -1 = thumbs down }); console.log(`Stored ${result.codeBlocksStored} code blocks`); ``` ### Parameters | Parameter | Type | Default | Description | | --------------------------------------------------- | ------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------ | | `task` | `string` | required | The task that was executed | | `file_written` / `fileWritten` | [FileWritten](#filewritten) | required | The file created during execution | | `succeeded` | `boolean` | required | Whether the task completed successfully | | `use_raysurfer_ai_voting` / `useRaysurferAiVoting` | `boolean` | `true` | Let Raysurfer AI vote on stored blocks. Ignored when `user_vote` is provided. | | `user_vote` / `userVote` | `int \| null` | `null` | User-provided vote: `1` for thumbs up, `-1` for thumbs down. When provided, AI voting is automatically skipped. | | `vote_source` / `voteSource` | `"ai" \| "human" \| null` | `null` | Explicitly sets the vote source. When omitted, `user_vote` defaults to `"human"` and AI voting defaults to `"ai"`. | | `vote_count` / `voteCount` | `int \| null` | `1` | Number of votes to apply. Useful for pre-seeding community votes. | | `execution_logs` / `executionLogs` | `string \| null` | `null` | Captured stdout/stderr for vote context | | `dependencies` | `dict[str, str]` / `Record` | `null` | Package dependencies with versions (e.g., `{"pandas": "2.1.0"}`) | | `per_function_reputation` / `perFunctionReputation` | `boolean` | `false` | Extract function fingerprints and track per-function reputation for this upload | ### Returns: [SubmitExecutionResultResponse](#submitexecutionresultresponse) When `use_raysurfer_ai_voting` is `true` (the default) and no `user_vote` is provided, each newly stored code block is automatically evaluated by Raysurfer's AI. If you provide a `user_vote`, AI voting is skipped entirely — your vote is applied directly. *** ## upload\_bulk\_code\_snips / uploadBulkCodeSnips Bulk upload prompts, logs, and code files for sandboxed grading. ```python Python theme={null} from raysurfer.types import FileWritten, LogFile files = [ FileWritten(path="app.py", content="def main(): ..."), FileWritten(path="utils.py", content="def helper(): ..."), ] logs = [ LogFile(path="logs/run.log", content="Task completed", encoding="utf-8"), ] # Let Raysurfer AI grade each file (default behavior) result = client.upload_bulk_code_snips( prompts=["Build a CLI tool", "Add CSV support"], files_written=files, log_files=logs, ) # Or provide your own votes (AI grading is automatically skipped) result = client.upload_bulk_code_snips( prompts=["Build a CLI tool", "Add CSV support"], files_written=files, log_files=logs, user_votes={ "app.py": 1, # thumbs up "utils.py": -1, # thumbs down }, ) ``` ```typescript TypeScript theme={null} const files = [ { path: "app.ts", content: "function main() { ... }" }, { path: "utils.ts", content: "function helper() { ... }" }, ]; const logs = [ { path: "logs/run.log", content: "Task completed", encoding: "utf-8" }, ]; // Let Raysurfer AI grade each file (default behavior) const result = await client.uploadBulkCodeSnips({ prompts: ["Build a CLI tool", "Add CSV support"], filesWritten: files, logFiles: logs, }); // Or provide your own votes (AI grading is automatically skipped) const result2 = await client.uploadBulkCodeSnips({ prompts: ["Build a CLI tool", "Add CSV support"], filesWritten: files, logFiles: logs, userVotes: { "app.ts": 1, "utils.ts": -1 }, }); ``` ### Parameters | Parameter | Type | Default | Description | | --------------------------------------------------- | ------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------- | | `prompts` | `string[]` | required | Ordered list of raw user prompts | | `files_written` / `filesWritten` | [FileWritten](#filewritten)`[]` | required | Code files to store and grade | | `log_files` / `logFiles` | [LogFile](#logfile)`[]` | `null` | Log files (any format; use `encoding: "base64"` for binary) | | `use_raysurfer_ai_voting` / `useRaysurferAiVoting` | `boolean` | `true` | Let Raysurfer AI grade and vote on stored blocks. Ignored when `user_votes` is provided. | | `user_votes` / `userVotes` | `dict[str, int] \| null` | `null` | Dict of filename to vote (`1` = thumbs up, `-1` = thumbs down). When provided, AI grading is automatically skipped. | | `vote_source` / `voteSource` | `"ai" \| "human" \| null` | `null` | Explicitly sets the vote source. When omitted, `user_votes` defaults to `"human"` and AI voting defaults to `"ai"`. | | `vote_count` / `voteCount` | `int \| null` | `1` | Number of votes to apply per file. Useful for pre-seeding community votes. | | `per_function_reputation` / `perFunctionReputation` | `boolean` | `false` | Extract function fingerprints and track per-function reputation for each uploaded file | ### Returns: [BulkExecutionResultResponse](#bulkexecutionresultresponse) *** ## delete Delete a snippet and all its associated data (vectors, retrieval logs, votes). ```python Python theme={null} # Delete by ID result = client.delete(snippet_id="cb_abc123") # Or delete by name result = client.delete(snippet_id="weather_fetcher.py") print(f"Deleted {result.deleted_count} snippet(s)") ``` ```typescript TypeScript theme={null} // Delete by ID const result = await client.delete("cb_abc123"); // Or delete by name const result2 = await client.delete("weather_fetcher.py"); console.log(`Deleted ${result.deletedCount} snippet(s)`); ``` ### Parameters | Parameter | Type | Default | Description | | ------------------------------ | -------- | -------- | --------------------------------------- | | `snippet_id` / `snippetId` | `string` | required | The ID or name of the snippet to delete | | `workspace_id` / `workspaceId` | `string` | `null` | Override client-level workspace ID | ### Returns: [DeleteResponse](#deleteresponse) *** ## vote\_code\_snip / voteCodeSnip Vote on whether a cached code snippet was useful. This triggers background voting on the backend. ```python Python theme={null} client.vote_code_snip( task="Fetch GitHub trending repos", code_block_id="abc123", code_block_name="github_fetcher", code_block_description="Fetches trending repos", succeeded=True, ) ``` ```typescript TypeScript theme={null} await client.voteCodeSnip({ task: "Fetch GitHub trending repos", codeBlockId: "abc123", codeBlockName: "github_fetcher", codeBlockDescription: "Fetches trending repos", succeeded: true, }); ``` ### Returns: [VoteCodeSnipResponse](#votecodesnipresponse) Voting is triggered automatically by `upload` when `use_raysurfer_ai_voting` is `true` (the default), and also by the high-level `RaysurferClient`. Use `vote_code_snip` directly only for voting on cached blocks after reuse. *** ## Response Types ### SearchResponse Returned by [`search()`](#search). | Field | Type | Description | | ---------------------------- | ------------------------------- | -------------------------------------- | | `matches` | [SearchMatch](#searchmatch)`[]` | Matching code blocks with scoring | | `total_found` / `totalFound` | `int` | Total matches found | | `cache_hit` / `cacheHit` | `boolean` | Whether results were served from cache | *** ### SubmitExecutionResultResponse Returned by [`upload()`](#upload). | Field | Type | Description | | ----------------------------------------- | ---------------- | ----------------------------------- | | `success` | `boolean` | Whether the operation succeeded | | `code_blocks_stored` / `codeBlocksStored` | `int` | Number of code blocks stored | | `message` | `string` | Human-readable status message | | `snippet_name` / `snippetName` | `string \| null` | Name assigned to the stored snippet | *** ### BulkExecutionResultResponse Returned by [`upload_bulk_code_snips()`](#upload_bulk_code_snips--uploadbulkcodesnips). | Field | Type | Description | | ----------------------------------------- | ---------------- | ------------------------------------------- | | `success` | `boolean` | Whether the operation succeeded | | `code_blocks_stored` / `codeBlocksStored` | `int` | Number of code blocks stored | | `votes_queued` / `votesQueued` | `int` | Number of votes queued for AI grading | | `message` | `string` | Human-readable status message | | `status_url` / `statusUrl` | `string \| null` | URL to check grading status (if applicable) | *** ### DeleteResponse Returned by [`delete()`](#delete). | Field | Type | Description | | -------------------------------- | --------- | ------------------------------- | | `success` | `boolean` | Whether the operation succeeded | | `deleted_count` / `deletedCount` | `int` | Number of snippets deleted | | `message` | `string` | Human-readable status message | *** ### VoteCodeSnipResponse Returned by [`vote_code_snip()`](#vote_code_snip--votecodesnip). | Field | Type | Description | | ------------------------------ | --------- | -------------------------------------------------- | | `success` | `boolean` | Whether the operation succeeded | | `vote_pending` / `votePending` | `boolean` | Whether the vote is being processed asynchronously | | `message` | `string` | Human-readable status message | *** ## Data Types ### SearchMatch A single result from [`search()`](#search). Contains a full [CodeBlock](#codeblock) plus a relevance score. | Field | Type | Description | | ---------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------- | | `code_block` / `codeBlock` | [CodeBlock](#codeblock) | The matched code block with full source and metadata | | `score` | `float` | Overall relevance score (0–1) | | `thumbs_up` / `thumbsUp` | `int` | Total positive votes on this code block | | `thumbs_down` / `thumbsDown` | `int` | Total negative votes on this code block | | `filename` | `string` | Generated filename (e.g., `"github_fetcher.py"`) | | `language` | `string` | Programming language (e.g., `"python"`, `"typescript"`) | | `entrypoint` | `string` | Main function name to call (e.g., `"fetch_repos"`) | | `dependencies` | `dict[str, str]` / `Record` | Package name to version (e.g., `{"httpx": "0.27.0"}`) | | `functions` | [FunctionReputation](#functionreputation)`[] \| null` | Per-function reputation and telemetry metadata (only when requested) | *** ### FunctionReputation Per-function reputation metadata returned on `SearchMatch.functions`. | Field | Type | Description | | -------------------------------------------- | ---------------- | ------------------------------------------- | | `fingerprint` | `string` | Stable function fingerprint | | `function_name` / `functionName` | `string` | Function name | | `signature` | `string` | Function signature text | | `execution_count` / `executionCount` | `int` | Number of tracked executions | | `thumbs_up` / `thumbsUp` | `int` | Positive votes attributed to this function | | `thumbs_down` / `thumbsDown` | `int` | Negative votes attributed to this function | | `last_success_at` / `lastSuccessAt` | `string \| null` | Last success timestamp (ISO 8601) | | `last_failure_at` / `lastFailureAt` | `string \| null` | Last failure timestamp (ISO 8601) | | `common_errors` / `commonErrors` | `string[]` | Frequent error messages | | `avg_output_size` / `avgOutputSize` | `float` | Average output size observed from telemetry | | `output_completeness` / `outputCompleteness` | `float` | Non-empty output ratio (0.0 to 1.0) | | `repeat_call_rate` / `repeatCallRate` | `float` | Repeat invocation ratio from telemetry | | `total_logged_calls` / `totalLoggedCalls` | `int` | Number of logged telemetry calls | | `author_agent` / `authorAgent` | `string \| null` | Authoring agent identifier when available | | `reviewed_by_human` / `reviewedByHuman` | `boolean` | Whether this function received human review | | `derived_from` / `derivedFrom` | `string \| null` | Parent fingerprint lineage, if derived | *** ### CodeBlock A stored code block with full metadata. Nested inside [SearchMatch](#searchmatch). | Field | Type | Description | | -------------------------------------- | ------------------------------------------- | ---------------------------------------------------------------- | | `id` | `string` | Unique identifier (UUID) | | `name` | `string` | Human-readable name (e.g., `"GitHub Trending Fetcher"`) | | `description` | `string` | What the code does | | `source` | `string` | The full source code | | `entrypoint` | `string` | Main function/method name | | `language` | `string` | Programming language | | `language_version` / `languageVersion` | `string \| null` | Language version (e.g., `"3.12"`) | | `dependencies` | `dict[str, str]` / `Record` | Package name to version (e.g., `{"pandas": "2.1.0"}`) | | `tags` | `string[]` | Searchable tags (e.g., `["github", "api"]`) | | `capabilities` | `string[]` | What the code can do (e.g., `["http_requests", "json_parsing"]`) | | `input_schema` / `inputSchema` | `JsonDict` / `Record` | JSON schema describing expected inputs | | `output_schema` / `outputSchema` | `JsonDict` / `Record` | JSON schema describing outputs | | `example_queries` / `exampleQueries` | `string[] \| null` | Example task queries this code is good for | | `created_at` / `createdAt` | `string \| null` | ISO 8601 timestamp of creation | | `updated_at` / `updatedAt` | `string \| null` | ISO 8601 timestamp of last update | *** ## Input Types ### FileWritten A file created during agent execution. Used by [`upload()`](#upload) and [`upload_bulk_code_snips()`](#upload_bulk_code_snips--uploadbulkcodesnips). | Field | Type | Description | | --------- | -------- | ------------------------------------ | | `path` | `string` | File path (e.g., `"fetch_repos.py"`) | | `content` | `string` | Full file contents as a string | *** ### LogFile A log file for bulk grading. Used by [`upload_bulk_code_snips()`](#upload_bulk_code_snips--uploadbulkcodesnips). | Field | Type | Default | Description | | ------------------------------ | --------------------- | --------- | --------------------------------------------------- | | `path` | `string` | required | File path (e.g., `"logs/run.log"`) | | `content` | `string` | required | File contents (plain text or base64-encoded binary) | | `encoding` | `"utf-8" \| "base64"` | `"utf-8"` | Content encoding. Use `"base64"` for binary files. | | `content_type` / `contentType` | `string \| null` | `null` | MIME type (e.g., `"application/json"`) | *** ## Enums ### ExecutionState Technical execution outcome. | Value | Description | | ----------- | ------------------------------- | | `COMPLETED` | Execution finished successfully | | `ERRORED` | Execution threw an error | | `TIMED_OUT` | Execution exceeded time limit | | `CANCELLED` | Execution was cancelled | ### AgentVerdict Agent's judgment on execution quality. | Value | Description | | ------------- | ------------------------ | | `THUMBS_UP` | Execution was useful | | `THUMBS_DOWN` | Execution was not useful | | `PENDING` | Not yet evaluated | ### SnipsDesired Scope of private snippets for retrieval. | Value | Required Tier | Description | | --------- | ----------------- | --------------------------------------------------- | | `company` | PRO or ENTERPRISE | Organization-level snippets | | `client` | PRO or ENTERPRISE | Client workspace snippets (requires `workspace_id`) | *** ## Exceptions Both SDKs include built-in retry logic with exponential backoff for transient failures (429, 5xx, network errors). You don't need to implement your own retry logic for these cases. ### RaySurferError Base exception for all Raysurfer errors. | Property | Type | Description | | --------- | -------- | ------------- | | `message` | `string` | Error message | ### APIError Raised when the API returns an error response. Extends [RaySurferError](#raysurfererror). | Property | Type | Description | | ---------------------------- | -------- | ---------------- | | `message` | `string` | Error message | | `status_code` / `statusCode` | `int` | HTTP status code | ### AuthenticationError Raised when the API key is invalid or missing. Extends [RaySurferError](#raysurfererror). | Property | Type | Description | | --------- | -------- | ------------- | | `message` | `string` | Error message | ### CacheUnavailableError Raised when the cache backend is unreachable or returns an unexpected error. Extends [RaySurferError](#raysurfererror). | Property | Type | Description | | --------- | -------- | ------------- | | `message` | `string` | Error message | ### RateLimitError Raised when the API rate limit is exceeded. The SDK automatically retries with exponential backoff before raising this error. Extends [RaySurferError](#raysurfererror). | Property | Type | Description | | ---------------------------- | ------------- | ------------------------------- | | `message` | `string` | Error message | | `retry_after` / `retryAfter` | `int \| null` | Seconds until rate limit resets | ### ValidationError Raised when request validation fails. Extends [RaySurferError](#raysurfererror). | Property | Type | Description | | --------- | ---------------- | -------------------------------- | | `message` | `string` | Error message | | `field` | `string \| null` | The field that failed validation | # Enterprise Key Management Source: https://docs.raysurfer.com/enterprise-key-management Self-service API key creation, listing, and revocation for enterprise organizations These endpoints are **enterprise-only**. Your API key must belong to an enterprise-tier organization, and you must be an **admin** or **owner** of that organization to use them. No global admin key is required. Book a 15-minute call and we'll get your team configured. Enterprise org admins can create, list, and revoke API keys for members of their organization using these three endpoints. The caller's identity and org are derived from the API key in the `Authorization` header. *** ## Create a key for an org member Creates an API key for a user (by email), adding them to the organization if they aren't already a member. ```bash theme={null} curl -X POST https://api.raysurfer.com/api/keys/org/create \ -H "Authorization: Bearer $RAYSURFER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "dev@example.com", "key_name": "Dev laptop", "role": "member" }' ``` | Field | Type | Default | Description | | ---------- | ------ | ---------- | -------------------------------------------------------------- | | `email` | string | required | Email of the target user | | `key_name` | string | required | Display name for the key | | `role` | string | `"member"` | `"member"` or `"admin"` — only org owners can assign `"admin"` | **Response:** ```json theme={null} { "id": "a1b2c3d4-...", "name": "Dev laptop", "key": "rs_live_abc123...", "key_prefix": "rs_live_abc123456789", "user_email": "dev@example.com", "organization_id": "org-uuid-...", "organization_name": "Acme Corp", "is_new_member": true, "created_at": "2025-06-01T12:00:00Z" } ``` The full API key is only returned once at creation time. Store it securely — it cannot be retrieved again. **Rate limit:** 20 requests/minute *** ## List all keys in the org Returns all active (non-revoked) API keys in the caller's organization, including the key prefix, user email, and timestamps. ```bash theme={null} curl -X POST https://api.raysurfer.com/api/keys/org/list \ -H "Authorization: Bearer $RAYSURFER_API_KEY" \ -H "Content-Type: application/json" ``` No request body is needed — the org is derived from the caller's API key. **Response:** ```json theme={null} { "keys": [ { "id": "a1b2c3d4-...", "name": "Dev laptop", "key_prefix": "rs_live_abc123456789", "user_email": "dev@example.com", "user_id": "user-uuid-...", "created_at": "2025-06-01T12:00:00Z", "last_used_at": "2025-06-10T09:30:00Z" } ], "organization_id": "org-uuid-..." } ``` **Rate limit:** 30 requests/minute *** ## Revoke a key Revokes an API key by ID. The key must belong to the caller's organization — you cannot revoke keys from other orgs. ```bash theme={null} curl -X POST https://api.raysurfer.com/api/keys/org/revoke \ -H "Authorization: Bearer $RAYSURFER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "key_id": "a1b2c3d4-..." }' ``` | Field | Type | Description | | -------- | ------ | ------------------------- | | `key_id` | string | UUID of the key to revoke | **Response:** ```json theme={null} { "success": true, "key_id": "a1b2c3d4-..." } ``` Returns `404` if the key is not found, already revoked, or belongs to a different organization. **Rate limit:** 10 requests/minute *** ## Error codes | Status | Code | Meaning | | ------ | ----------------------------------- | --------------------------------------- | | 401 | `auth/missing_api_key` | No API key provided | | 401 | `auth/invalid_api_key` | Key is invalid or revoked | | 403 | `permission/not_org_member` | Caller's key is not linked to an org | | 403 | `permission/enterprise_required` | Org is not on the enterprise tier | | 403 | `permission/admin_key_required` | Caller is not an admin/owner of the org | | 403 | `permission/only_owner_can_promote` | Only owners can assign the admin role | | 400 | `validation/member_limit_reached` | Org has hit its member cap | | 404 | `not_found/api_key` | Key not found or already revoked | # Multi-Tenant Isolation of Code Snippets Source: https://docs.raysurfer.com/enterprise-multi-tenant Isolate cached code snippets per customer with workspaces Multi-tenant isolation is available on **Pro** and **Enterprise** tiers. Want private code snippets for your team? Book a 15-minute call to get set up. ## Overview If you're building a product that serves multiple customers, you need their cached snippets to be completely isolated. Customer A should never see Customer B's code. Raysurfer workspaces provide this isolation. Each workspace is a completely separate cache — snippets uploaded to one workspace can never be retrieved from another. *** ## Setup Pass your customer's ID as `workspace_id`: ```python Python theme={null} from raysurfer import RaysurferClient from claude_agent_sdk import ClaudeAgentOptions options = ClaudeAgentOptions(allowed_tools=["Read", "Write", "Bash"]) # Each customer gets their own isolated workspace async with RaysurferClient(options, workspace_id="acme") as client: await client.query("Process shipment data") async for msg in client.response(): print(msg) ``` ```typescript TypeScript theme={null} import { RaysurferClient } from "raysurfer"; // Each customer gets their own isolated workspace const client = new RaysurferClient({ allowedTools: ["Read", "Write", "Bash"], workspaceId: "acme", }); for await (const msg of client.query("Process shipment data")) { console.log(msg); } ``` That's it. All uploads and retrievals are now isolated to that customer. *** ## Low-Level API For direct snippet management, pass `workspace_id` either at the client level or per-method: ```python Python theme={null} from raysurfer import AsyncRaySurfer from raysurfer.types import FileWritten # Option 1: Set workspace_id at client level client = AsyncRaySurfer(api_key="rs_...", workspace_id="acme") results = await client.search(task="Process shipment data", top_k=5) # Option 2: Set workspace_id per-method (overrides client level) client = AsyncRaySurfer(api_key="rs_...") results = await client.search(task="Process shipment data", top_k=5, workspace_id="acme") # Upload - stores only to customer's isolated workspace await client.upload( task="Process shipment data", file_written=FileWritten(path="processor.py", content="def process(): ..."), succeeded=True, workspace_id="acme", # Per-method override ) ``` ```typescript TypeScript theme={null} import { RaySurfer } from "raysurfer"; // Option 1: Set workspaceId at client level const client = new RaySurfer({ apiKey: "rs_...", workspaceId: "acme" }); const results = await client.search({ task: "Process shipment data", topK: 5 }); // Option 2: Set workspaceId per-method (overrides client level) const client2 = new RaySurfer({ apiKey: "rs_..." }); const results2 = await client2.search({ task: "Process shipment data", topK: 5, workspaceId: "acme" }); // Upload - stores only to customer's isolated workspace await client2.upload({ task: "Process shipment data", fileWritten: { path: "processor.ts", content: "function process() { ... }" }, succeeded: true, workspaceId: "acme", // per-method override }); ``` *** ## Example: Multi-Tenant App ```python Python theme={null} from raysurfer import RaysurferClient from claude_agent_sdk import ClaudeAgentOptions async def handle_customer_request(customer_id: str, task: str): options = ClaudeAgentOptions(allowed_tools=["Read", "Write", "Bash"]) # Pass the customer ID as workspace_id async with RaysurferClient(options, workspace_id=customer_id) as client: await client.query(task) async for msg in client.response(): yield msg ``` ```typescript TypeScript theme={null} import { RaysurferClient } from "raysurfer"; async function* handleCustomerRequest(customerId: string, task: string) { // Pass the customer ID as workspaceId const client = new RaysurferClient({ allowedTools: ["Read", "Write", "Bash"], workspaceId: customerId, }); for await (const msg of client.query(task)) { yield msg; } } ``` *** ## FAQ ### Can snippets leak between workspaces? No. Workspaces are completely isolated. There is no code path that allows cross-workspace access. ### What if I don't set a workspace\_id? Your organization's shared cache is used. All users in your org see the same snippets — fine for internal use, but not for multi-tenant scenarios. ### What does a workspace actually control? A workspace is just a cache partition — it siloes which cached code snippets are visible, nothing more. Raysurfer does not manage your underlying permissions (database access, column-level security, API scopes, etc.). Your application decides which `workspace_id` to assign each user based on whatever permission model you already have. For example, if user A can query columns M and N, and user B can query columns M, N, O, and P, give them different workspace IDs (e.g. `"mn"` and `"mnop"`). User A's cached scripts will only ever reference their allowed columns, and user B's are completely separate. Raysurfer guarantees no cross-workspace leakage — your app still enforces the actual data permissions. ### What should I use as the workspace\_id? Any string that uniquely maps to a permission boundary — a customer ID, a role name, a hash of allowed scopes, or a permission profile key. It's an arbitrary string you choose. ### How does workspace search work? When you provide a `workspace_id`, Raysurfer searches both the workspace namespace and the org-wide namespace in parallel, merges results by score, and returns the top results. This means workspace-specific snippets compete on equal footing with company-wide snippets. ### Can I query multiple workspaces at once? No. Each client is scoped to one workspace. Query separately if needed. # How It Works Source: https://docs.raysurfer.com/how-it-works How Raysurfer retrieves proven code instead of regenerating from scratch ## AI Maintained Skills Raysurfer provides AI maintained skills for vertical agents — retrieve proven code from prior runs instead of regenerating from scratch. Every snippet earns a reputation through execution results and community voting, so the best code rises to the top. ``` Without Raysurfer: Task → Agent generates code from scratch → Runs it With Raysurfer: Task → Agent searches for proven code → Runs it (instant) ``` Queries don't need to be identical — Raysurfer uses semantic matching to find related patterns. "Fetch user data from the API" and "Get customer info from the endpoint" return the same proven code. ## The Ideal Pattern The best way to understand Raysurfer's value is through this pattern: ```python theme={null} llm_input = params out1 = query_customer_records_from_postgres_database(llm_input) out2 = lookup_contact_details_in_local_csv_file(out1[2]) out3 = fetch_purchase_history_from_stripe_api(out2.files[-1]) out4 = calculate_loyalty_tier_from_transaction_count(out3.length) return out4 ``` This is a multi-step workflow where each step depends on the previous step's output: 1. **First run**: The agent generates this code, executes it, and Raysurfer caches the entire block 2. **Second run**: Raysurfer retrieves the cached code and executes it directly — no LLM regeneration needed Without Raysurfer, the agent iterates through the LLM multiple times: generating code, checking if it compiles, running it, handling errors, retrying. Each iteration costs tokens and time. With Raysurfer, the agent retrieves proven code and runs it immediately — eliminating the trial-and-error loop entirely. ## The Loop Every execution follows this cycle: ``` Search proven code → Execute → Score → Promote top patterns ``` Over time, agents build up a library of proven code. High-reputation snippets get prioritized, and the agent starts from high-signal code paths instead of improvising each time. ## Under the Hood The mechanism behind this is a semantic code cache. When your agent generates code that works, Raysurfer caches it with semantic embeddings. When a similar task comes up later — from you or from the community — the proven code is retrieved instead of regenerated. You don't need to manage anything. Caching and retrieval happen automatically. ## Why This Works Production agents in 2026 are running longer and longer — multi-step workflows, complex tool chains, sprawling context windows. But here's the thing: **the median run has a typical shape.** Despite all the complexity, most runs follow similar patterns. Raysurfer captures these common patterns as proven code that your agent reuses instead of regenerating. This gives you: * **Consistent behavior** — the agent starts from proven code instead of improvising each time * **Better context management** — intermediate outputs between API calls aren't printed by default, keeping context clean * **Faster execution** — skip the generation, go straight to the result ## Well-Documented Tool Schemas For cached code to be reusable, the tool functions it calls must have well-documented input and output schemas. This lets agents extract specific properties from one tool's response to pass into the next. ```python Good - Specific types theme={null} from dataclasses import dataclass @dataclass class SalesRecord: id: str amount: float quarter: str region: str @dataclass class Report: title: str records: list[SalesRecord] total: float def generate_report(records: list[SalesRecord], template: str) -> Report: ... ``` ```python Bad - Vague types theme={null} def generate_report(records: list[dict], template: str) -> dict: # Agent doesn't know the shape of records or return value ... def process_data(data: Any) -> Any: # Impossible to generate correct downstream code ... ``` When types are specific, the agent generates code that correctly accesses fields like `record.amount` instead of guessing with `record["amount"]` or `record.get("amount", 0)`. ## Quality Over Time Raysurfer tracks which cached outputs work well: * Outputs that produce useful results get prioritized * Outputs that fail or produce unhelpful results get deprioritized * The system improves automatically Reputation scores track whether outputs were genuinely helpful to users, not just whether they executed successfully. Code that technically runs but doesn't solve the user's problem gets deprioritized. ## Verified Snippets Every cached output is verified before reuse: * Only successful executions get cached * Failed outputs are automatically excluded * Your agent builds a library of proven code over time ## Immutable Snippets Code snippets in Raysurfer are immutable — they cannot be edited after creation. When you need a modified version of existing code, a new snippet is created instead. This means: * **Edits create new snippets** — modifying cached code doesn't overwrite the original; it creates a new entry that surfaces for different types of requests * **Original code stays intact** — the original snippet remains available with its existing reputation scores * **Version history is automatic** — each iteration of a code pattern becomes its own snippet, and the best-performing version rises to the top through voting You can view all your snippets in the [dashboard](https://www.raysurfer.com/dashboard). ## Snippet Scoring Every snippet has a reputation score that determines its priority in retrieval. ### Automatic Voting The system automatically votes in two scenarios: * **On cache reuse** — when cached code runs successfully and produces the expected output, it receives a thumbs up; failures receive a thumbs down * **On upload** — when new code is stored after a successful execution, Raysurfer's AI evaluates and votes on it (controlled by `use_raysurfer_ai_voting`, on by default). You can also provide your own votes via `user_vote` / `user_votes`, which skips AI voting. Both happen in the background after each execution — you don't need to do anything. The SDK also captures execution logs (tool outputs) and sends them to the backend as context for voting. ### Manual Voting You can also manually adjust scores in the [dashboard](https://www.raysurfer.com/dashboard): * Add positive score to promote high-quality snippets * Add negative score to demote problematic code * Override automatic votes when you know better than the AI The reputation score is calculated as: ``` reputation_score = thumbs_up / (thumbs_up + thumbs_down) ``` Snippets with higher reputation scores are prioritized during retrieval, ensuring your agent uses the most reliable code. ## Per-Function Signals Snippet-level votes tell you whether the overall script was useful. For iterative agents, you can also enable per-function reputation to surface diagnostics inside each function and track function-level telemetry. Use `per_function_reputation` / `perFunctionReputation` on upload and search. See [Per-Function Reputation](/per-function-reputation) for copy-paste examples. ## Public Snippets — The Community Registry The skills library extends to a public registry of validated code patterns sourced from popular open-source GitHub repositories. These are community-contributed skills that any agent can search and use. Enable public snippets by setting `public_snips` in the SDK constructor: ```python Python theme={null} from raysurfer import RaySurfer client = RaySurfer(api_key="your_key", public_snips=True) result = client.search(task="Parse CSV and generate chart") ``` ```typescript TypeScript theme={null} import { RaySurfer } from "raysurfer"; const client = new RaySurfer({ apiKey: "your_key", publicSnips: true }); const result = await client.search({ task: "Parse CSV and generate chart" }); ``` Public snippets are read-only — they cannot be edited or deleted by users. They are curated by the Raysurfer team and ranked alongside your private snippets using the same voting system — the best answers rise to the top. Also available via the [CLI](/integrations/cli) (`--public` flag) and [MCP server](/integrations/mcp) (`public_snips` parameter). # Introduction Source: https://docs.raysurfer.com/index AI maintained skills for vertical agents # What is Raysurfer? Raysurfer provides **AI maintained skills for vertical agents**. Your agent stores scripts from prior runs, searches them semantically, and reuses what worked — instead of regenerating from scratch every time. Your agent runs proven code that already worked, not a fresh generation roll. Same correct output every time. Proven code retrieved in \~1s vs 30-180s of regenerating from scratch Skip token-expensive code generation on tasks your agent has already solved Public registry of validated code patterns, plus your own private library ## The Problem Production agents chain together 10, 20, 30+ tool calls per run. A freight logistics agent reads a row from the database, pulls a column, checks a Slack channel, cross-references an email, control-F's a date — that's 10 tools before it even has the right context. Then it still has to act on it. Each tool call is an LLM roundtrip. Each roundtrip can fail. And errors compound across the chain: even if each step has a 98% success rate, a 20-step workflow only has a 67% chance of getting through cleanly. At 50 tools, you're at 36%. You're paying for tokens, waiting for generation, and getting inconsistent results — for code patterns your agent has already solved hundreds of times before. ## The Solution Raysurfer caches the code your agent generates. When a similar task comes up, the agent retrieves the entire proven workflow and runs it directly — no LLM regeneration, no trial-and-error loop, no compounding errors. ``` Without Raysurfer: Task → 30 serial LLM roundtrips → errors compound → maybe works With Raysurfer: Task → retrieve proven code → runs immediately → same result every time ``` Think of it as compiling your agent's tool call sequences into reusable code. Once a workflow is proven, the agent doesn't need the LLM anymore for that task — it just runs the code. Queries don't need to be identical — Raysurfer uses semantic matching to find related patterns. "Fetch GitHub repos" and "get trending GitHub projects" return the same proven code. ## Why Not Just Use Skills? Anthropic and other frameworks push "skills" — developer-maintained markdown or code that agents can load. Skills get you most of the way there, but they're **developer-maintained, not AI-maintained.** If you have 50 clients and each client has 20 common tool call patterns, you're building and maintaining 1,000 skills by hand. That's not scalable. Raysurfer skills are AI-maintained. The agent generates code, executes it, and Raysurfer caches what works. No developer writes or maintains the cached workflows — the agent does it itself, and the best code rises to the top through reputation scoring. ## How It Works 1. **Search for proven code** — Describe your task in natural language. Raysurfer finds code that already solved similar tasks. 2. **Run it** — Retrieved code is proven to work. Your agent runs it immediately. 3. **Build reputation** — Code that produces useful results gets promoted. Code that fails gets deprioritized. The best answers rise to the top. Raysurfer helps agents running 10+ tool calls per run. Check if your workflow is a fit. Follow the quickstart to integrate Raysurfer in under 5 minutes Summary-first hashed logs agents can expand with authenticated curl # Browser Agent Source: https://docs.raysurfer.com/integrations/browser-agent Cache and reuse Playwright scripts across browser automation runs Wrap any browser automation agent with Raysurfer's `search()` and `upload()` to cache Playwright scripts. On repeat tasks, the LLM receives the cached script as context and decides whether to reuse it, modify it, or generate a new one. Cached scripts are always reviewed by the LLM before execution — never run directly. ## How It Works ```text theme={null} 1. search(task) → find cached Playwright scripts for this task 2. LLM reviews → decides: reuse, modify, or generate fresh 3. Execute script → run Playwright in headless browser 4. upload(task, script) → cache successful scripts for next time ``` First run generates a script from scratch. Subsequent runs retrieve the cached script and let the LLM decide. ## Setup ```bash theme={null} pip install raysurfer anthropic playwright playwright install chromium export ANTHROPIC_API_KEY=sk-ant-... export RAYSURFER_API_KEY=rs_... ``` ## Quick Example ````python theme={null} import asyncio import os from raysurfer import AsyncRaySurfer, FileWritten from anthropic import AsyncAnthropic SYSTEM_PROMPT = ( "You are a browser automation expert. Write Python scripts using " "playwright.sync_api with chromium in headless mode. " "Print the final result to stdout. " "Respond with ONLY a JSON object: {\"script\": \"\", \"explanation\": \"\"}" ) async def run_browser_task(task: str, url: str | None = None): rs = AsyncRaySurfer(api_key=os.environ["RAYSURFER_API_KEY"]) anthropic = AsyncAnthropic() # 1. Search for cached scripts cached_script = None response = await rs.search(task=task, top_k=1, min_verdict_score=0.3) if response.matches: cached_script = response.matches[0].code_block.source # 2. Build prompt — include cached script if found url_line = f"\nStarting URL: {url}" if url else "" if cached_script: user_msg = ( f"Task: {task}{url_line}\n\n" f"A cached script for a similar task:\n```python\n{cached_script}\n```\n\n" "Review it: run as-is if it fits, modify for the current task, " "or write a new script if not relevant." ) else: user_msg = f"Task: {task}{url_line}" # 3. LLM generates or approves the script llm_response = await anthropic.messages.create( model="claude-sonnet-4-20250514", max_tokens=4096, system=SYSTEM_PROMPT, messages=[{"role": "user", "content": user_msg}], ) import json parsed = json.loads(llm_response.content[0].text) script = parsed["script"] # 4. Execute the Playwright script import subprocess, sys, tempfile with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: f.write(script) f.flush() result = subprocess.run([sys.executable, f.name], capture_output=True, text=True, timeout=60) if result.returncode == 0: print(f"Output: {result.stdout.strip()}") # 5. Cache the successful script await rs.upload( task=task, file_written=FileWritten(path="browser_script.py", content=script), succeeded=True, execution_logs=result.stdout, tags=["browser-automation", "playwright"], ) else: print(f"Error: {result.stderr.strip()}") await rs.close() asyncio.run(run_browser_task("Get the titles and prices of the first 5 books", url="https://books.toscrape.com")) ```` ## What Gets Cached The cached artifact is a complete, runnable Playwright Python script: ```python theme={null} from playwright.sync_api import sync_playwright import json def scrape_books(): with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto('https://books.toscrape.com', timeout=10000) page.wait_for_load_state('networkidle') books = [] for book in page.locator('article.product_pod').all()[:5]: title = book.locator('h3 a').get_attribute('title') price = book.locator('p.price_color').inner_text() books.append({'title': title, 'price': price}) print(json.dumps(books, indent=2)) browser.close() if __name__ == '__main__': scrape_books() ``` On the next similar request, this script is passed to the LLM as context. The LLM can reuse it directly or modify it for the new task. ## Handling Optional Elements Real pages have cookie banners, login modals, age gates, and other elements that may or may not appear. Cached scripts should check for these before interacting: ```python theme={null} from playwright.sync_api import sync_playwright def scrape_with_guards(): with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto('https://example-store.com', timeout=15000) page.wait_for_load_state('networkidle') # Cookie consent banner cookie_btn = page.locator('button:has-text("Accept"), #cookie-accept, [data-testid="cookie-accept"]') if cookie_btn.count() > 0: cookie_btn.first.click() page.wait_for_timeout(500) # Age verification gate age_gate = page.locator('[data-testid="age-gate"], .age-verification') if age_gate.count() > 0: confirm_btn = age_gate.locator('button:has-text("Yes"), button:has-text("I am over")') if confirm_btn.count() > 0: confirm_btn.first.click() page.wait_for_timeout(500) # Dismiss newsletter/promo popup popup_close = page.locator('[aria-label="Close"], .modal-close, .popup-close, button:has-text("No thanks")') if popup_close.count() > 0: popup_close.first.click() page.wait_for_timeout(500) # "Load more" button — click until all items are visible or button disappears while True: load_more = page.locator('button:has-text("Load more"), button:has-text("Show more")') if load_more.count() == 0 or not load_more.first.is_visible(): break load_more.first.click() page.wait_for_timeout(1000) # Login wall — skip scraping if login is required login_wall = page.locator('form[action*="login"], [data-testid="login-required"]') if login_wall.count() > 0: print("Login required — cannot proceed") browser.close() return # Now scrape the actual content items = page.locator('.product-card').all() results = [] for item in items[:10]: name = item.locator('.product-name') price = item.locator('.product-price') results.append({ 'name': name.inner_text() if name.count() > 0 else 'N/A', 'price': price.inner_text() if price.count() > 0 else 'N/A', }) import json print(json.dumps(results, indent=2)) browser.close() if __name__ == '__main__': scrape_with_guards() ``` Common patterns to guard against: | Element | How to detect | What to do | | ------------------ | --------------------------------------------- | ------------------------------ | | Cookie consent | `button:has-text("Accept")`, `#cookie-accept` | Click accept, wait briefly | | Newsletter popup | `.modal-close`, `[aria-label="Close"]` | Click close | | Age verification | `.age-verification`, `data-testid="age-gate"` | Click confirm | | "Load more" button | `button:has-text("Load more")` | Click in loop until gone | | Login wall | `form[action*="login"]` | Exit early or log in | | CAPTCHA | `.captcha`, `iframe[src*="recaptcha"]` | Exit early — can't automate | | Region selector | `.region-picker`, `select[name="country"]` | Select region, wait for reload | These guards get cached alongside the scraping logic, so subsequent runs of the same task handle the same popups automatically. ## Standalone Wrapper For a ready-to-use wrapper class, see the [raysurfer-browser-agent example](https://github.com/rayxc-org/raysurfer/tree/master/examples/raysurfer-browser-agent): ```python theme={null} from raysurfer_browser_agent import RaysurferBrowserAgent async with RaysurferBrowserAgent() as agent: # First run: generates script from scratch, caches it result = await agent.run("Get the top 3 stories from Hacker News") # Second run: LLM reviews cached script and reuses it result = await agent.run("Get the top 3 stories from Hacker News") print(result.output) # scraped data print(result.cached) # True — cached script was found print(result.reused_cache) # True — LLM chose to reuse it ``` ## Related * [Wrap Any LLM Tool Flow](/integrations/llm-tool-flows) * [Python SDK](/sdk/python) * [Quickstart](/quickstart) # CLI Source: https://docs.raysurfer.com/integrations/cli Search, upload, and vote on cached code from the command line The Raysurfer CLI (`raysurfer`) is a standalone Python tool for interacting with the code cache. Use it in scripts, CI pipelines, or directly from your terminal. Source: [rayxc-org/raysurfer-code-caching-cli](https://github.com/rayxc-org/raysurfer-code-caching-cli) ## Installation ```bash pip theme={null} pip install raysurfer-code-caching-cli ``` ```bash uv theme={null} uv pip install raysurfer-code-caching-cli ``` ## Setup ```bash theme={null} export RAYSURFER_API_KEY=your_key_here ``` ## Commands ### Search Find cached code matching a task description: ```bash theme={null} raysurfer search "Generate a quarterly revenue report" ``` Show the source code from the top match: ```bash theme={null} raysurfer search "Generate a quarterly revenue report" --show-code ``` Get JSON output for scripting: ```bash theme={null} raysurfer search "Generate a quarterly revenue report" --json ``` Include community public snippets in results: ```bash theme={null} raysurfer search "Generate a quarterly revenue report" --public ``` | Flag | Description | Default | | ------------------- | --------------------------------- | ------- | | `--top-k`, `-k` | Maximum results | 5 | | `--min-score`, `-s` | Minimum verdict score (0-1) | 0.3 | | `--public`, `-p` | Include community public snippets | off | | `--show-code`, `-c` | Display source from top match | off | | `--json`, `-j` | Output as JSON | off | ### Upload Upload code files to the cache after a successful execution: ```bash theme={null} raysurfer upload "Generate a quarterly revenue report" --file report.py ``` Upload multiple files: ```bash theme={null} raysurfer upload "Build a dashboard" --file app.py --file utils.py ``` | Flag | Description | Default | | ---------------------- | ---------------------------------- | --------- | | `--file`, `-f` | File to upload (repeatable) | required | | `--succeeded/--failed` | Whether the execution succeeded | succeeded | | `--no-auto-vote` | Disable automatic upvote on upload | off | ### Vote Provide feedback on cached code quality: ```bash theme={null} # Upvote raysurfer vote cb_abc123 --up # Downvote raysurfer vote cb_abc123 --down ``` | Flag | Description | Default | | -------------- | ------------------------- | ------- | | `--up/--down` | Upvote or downvote | up | | `--task`, `-t` | Original task description | none | ### Patterns Get proven task-to-code mappings: ```bash theme={null} raysurfer patterns "data analysis" ``` ### Examples Get few-shot examples for prompting: ```bash theme={null} raysurfer examples "data analysis" ``` ### Version ```bash theme={null} raysurfer version raysurfer version --json ``` ## Python Library The CLI also exports sync and async clients for use in your own code: ```python Sync theme={null} from raysurfer_cli.client import RaysurferClient with RaysurferClient() as client: result = client.search(task="Generate a quarterly report") for match in result.matches: print(match.code_block.name, match.combined_score) ``` ```python Async theme={null} from raysurfer_cli.client import AsyncRaysurferClient async with AsyncRaysurferClient() as client: result = await client.search(task="Generate a quarterly report") for match in result.matches: print(match.code_block.name, match.combined_score) ``` ## Configuration | Environment Variable | Description | Default | | -------------------- | -------------------------- | --------------------------- | | `RAYSURFER_API_KEY` | API key (required) | none | | `RAYSURFER_BASE_URL` | API base URL | `https://api.raysurfer.com` | | `RAYSURFER_TIMEOUT` | Request timeout in seconds | 30 | # Function Registry Source: https://docs.raysurfer.com/integrations/function-registry Publish a registry of your functions so agents can discover and call them across sessions Publish a registry of your functions so agents can discover and call them across sessions. Raysurfer auto-infers the function name, description, and input schema from the function signature, type hints, and docstring — so you don't need to write schemas by hand. Once registered, functions can be: * Published to Raysurfer for agent discovery via `publish_function_registry` / `publishFunctionRegistry` * Converted to Anthropic tool definitions via `to_anthropic_tool` / `toAnthropicTool` for direct use with Claude's tool calling * Tracked automatically — every call records execution duration, inputs, outputs, and success/failure ## Which Path Passes Functions Back To The Running Agent? These three paths are related, but they are not the same runtime: | Path | Purpose | How functions are wired at runtime | | ------------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | `publish_function_registry` / `publishFunctionRegistry` | Persist reusable functions for discovery across sessions | Functions are uploaded as registry snippets and made available to Raysurfer sandbox runs for that scope | | `to_anthropic_tool` / `toAnthropicTool` | Generate Anthropic-compatible tool schemas | You pass the returned schema to your Claude tool-calling loop and handle tool execution in your app | | `@rs.tool` / `tool(...)` + `execute(...)` | Live tool calling in Raysurfer sandbox | Raysurfer opens a callback channel and routes each tool call back to your local callback during execution | If your question is "how does the function call get passed back into my agent process during execution?", use `@rs.tool` / `tool(...)` with `execute(...)`. See [Register Python Functions](/integrations/register-python-functions) or [Register TypeScript Functions](/integrations/register-ts-functions). ## Python ### Auto-inferred (no explicit params) ```python theme={null} from raysurfer import AsyncRaySurfer, agent_accessible rs = AsyncRaySurfer() @agent_accessible() def generate_board_report(company: str, quarter: str) -> str: """Generate a quarterly board report. Args: company: The company name to generate the report for. quarter: The fiscal quarter (e.g. "Q1 2026"). """ return build_report(company=company, quarter=quarter) ``` Name, description, and input schema are all inferred from the function name, docstring, and type hints. ### With explicit description ```python theme={null} @agent_accessible("Generate a quarterly board report") def generate_board_report(company: str, quarter: str) -> str: return build_report(company=company, quarter=quarter) ``` ### Org/workspace scoping ```python theme={null} @agent_accessible(org_id="acme-corp", workspace_id="finance-team") def generate_board_report(company: str, quarter: str) -> str: """Generate a quarterly board report.""" return build_report(company=company, quarter=quarter) ``` ### Converting to Anthropic tool format ```python theme={null} from raysurfer import agent_accessible, to_anthropic_tool @agent_accessible() def generate_board_report(company: str, quarter: str) -> str: """Generate a quarterly board report.""" return build_report(company=company, quarter=quarter) tool = to_anthropic_tool(generate_board_report) # Returns: {"name": "generate_board_report", "description": "...", "input_schema": {...}} ``` ### Publishing to Raysurfer ```python theme={null} rs = AsyncRaySurfer() await rs.publish_function_registry([generate_board_report]) ``` ## TypeScript ### Auto-inferred (no explicit options) ```typescript theme={null} import { RaySurfer, agentAccessible } from "raysurfer"; const rs = new RaySurfer(); const generateBoardReport = agentAccessible( (company: string, quarter: string) => buildReport(company, quarter), ); ``` Name and parameters are inferred from the function. ### With explicit options ```typescript theme={null} const generateBoardReport = agentAccessible( (company: string, quarter: string) => buildReport(company, quarter), { name: "generateBoardReport", description: "Generate a quarterly board report", }, ); ``` ### Org/workspace scoping ```typescript theme={null} const generateBoardReport = agentAccessible( (company: string, quarter: string) => buildReport(company, quarter), { orgId: "acme-corp", workspaceId: "finance-team" }, ); ``` ### Converting to Anthropic tool format ```typescript theme={null} import { agentAccessible, toAnthropicTool } from "raysurfer"; const generateBoardReport = agentAccessible( (company: string, quarter: string) => buildReport(company, quarter), { description: "Generate a quarterly board report" }, ); const tool = toAnthropicTool(generateBoardReport); // Returns: { name: "generateBoardReport", description: "...", input_schema: {...} } ``` ### Publishing to Raysurfer ```typescript theme={null} const rs = new RaySurfer(); await rs.publishFunctionRegistry([generateBoardReport]); ``` ## Persistent Sandbox Previously published functions are available in the sandbox execution environment, scoped by org and workspace. When an agent runs code through Raysurfer's sandbox, functions published via `publish_function_registry` / `publishFunctionRegistry` for that scope are available without re-uploading each session. For live callback routing during a run, use `@rs.tool` / `tool(...)` with `execute(...)`. ## Running In Raysurfer Sandbox To run a coding agent in Raysurfer's remote sandbox with tool callbacks: 1. Register callbacks with `@rs.tool` (Python) or `tool(...)` (TypeScript). 2. Call `execute(...)` with either `user_code` / `userCode` or optional sandbox codegen inputs. 3. Handle results from `ExecuteResult` (`result`, `error`, and `tool_calls` / `toolCalls`). Use the end-to-end guides: * [Register Python Functions](/integrations/register-python-functions) * [Register TypeScript Functions](/integrations/register-ts-functions) # Wrap Any LLM Tool Flow Source: https://docs.raysurfer.com/integrations/llm-tool-flows Runnable quickstarts for wrapping search() and upload() around Anthropic, LangSmith, and custom tool-calling loops If you already have an LLM tool flow, Raysurfer stays simple: 1. `search()` before the run 2. `upload()` after the run That pattern works whether you call Anthropic directly, run through LangSmith/LangChain, or use your own orchestrator. Core idea: two calls around your existing flow, no retriever plumbing needed. ## Quickstart Pattern ```text theme={null} search(task) -> run your tool flow -> upload(task, changed_code, logs) ``` ## Python: Anthropic Tool Flow ````python theme={null} import asyncio from anthropic import AsyncAnthropic from raysurfer import AsyncRaySurfer, FileWritten def build_cached_context(matches) -> str: lines = ["Use these cached snippets before generating new code:"] for match in matches[:5]: lines.append(f"\n### {match.filename}") lines.append(f"```{match.language}\n{match.code_block.source}\n```") return "\n".join(lines) async def run_task(task: str) -> None: rs = AsyncRaySurfer() # uses RAYSURFER_API_KEY anthropic = AsyncAnthropic() # 1) Pull top snippets for this task search_result = await rs.search(task=task, top_k=5) cached_context = build_cached_context(search_result.matches) # 2) Run your existing model/tool flow response = await anthropic.messages.create( model="claude-sonnet-4-5", max_tokens=1200, system=cached_context, messages=[{"role": "user", "content": task}], ) final_text = "".join( block.text for block in response.content if getattr(block, "type", "") == "text" ) # Example changed code produced by your flow generated_code = f"print({final_text!r})\n" # 3) Upload changed code + logs await rs.upload( task=task, file_written=FileWritten(path="generated.py", content=generated_code), succeeded=True, execution_logs=final_text, ) await rs.close() asyncio.run(run_task("Generate a quarterly report from CSV files")) ```` ## TypeScript: Anthropic Tool Flow ```typescript theme={null} import Anthropic from "@anthropic-ai/sdk"; import { RaySurfer } from "raysurfer"; function buildCachedContext(matches: Array<{ filename: string; language: string; codeBlock: { source: string }; }>): string { const lines = ["Use these cached snippets before generating new code:"]; for (const match of matches.slice(0, 5)) { lines.push(`\n### ${match.filename}`); lines.push(`\`\`\`${match.language}\n${match.codeBlock.source}\n\`\`\``); } return lines.join("\n"); } async function runTask(task: string): Promise { const rs = new RaySurfer(); // uses RAYSURFER_API_KEY const anthropic = new Anthropic(); // 1) Pull top snippets for this task const searchResult = await rs.search({ task, topK: 5 }); const cachedContext = buildCachedContext(searchResult.matches); // 2) Run your existing model/tool flow const response = await anthropic.messages.create({ model: "claude-sonnet-4-5", max_tokens: 1200, system: cachedContext, messages: [{ role: "user", content: task }], }); let finalText = ""; for (const block of response.content) { if (block.type === "text") { finalText += block.text; } } // Example changed code produced by your flow const generatedCode = `console.log(${JSON.stringify(finalText)});\\n`; // 3) Upload changed code + logs await rs.upload({ task, fileWritten: { path: "generated.ts", content: generatedCode }, succeeded: true, executionLogs: finalText, }); } await runTask("Generate a quarterly report from CSV files"); ``` ## LangSmith / Framework Wrapper Pattern Use the same two-call shape around your existing runner: ```python theme={null} async def run_with_raysurfer(task: str, run_flow): rs = AsyncRaySurfer() # before search_result = await rs.search(task=task, top_k=5) cached_context = build_cached_context(search_result.matches) # your framework flow (LangSmith/LangChain/custom) result = await run_flow(task=task, cached_context=cached_context) # expected result shape: # result.code_path, result.code_content, result.logs, result.succeeded # after await rs.upload( task=task, file_written=FileWritten(path=result.code_path, content=result.code_content), succeeded=result.succeeded, execution_logs=result.logs, ) ``` ## Workspace Silo (Enterprise) When you need per-customer isolation, pass `workspace_id`/`workspaceId` on both calls: ```python Python theme={null} await rs.search(task=task, top_k=5, workspace_id="acme") await rs.upload(task=task, file_written=file, succeeded=True, workspace_id="acme") ``` ```typescript TypeScript theme={null} await rs.search({ task, topK: 5, workspaceId: "acme" }); await rs.upload({ task, fileWritten: file, succeeded: true, workspaceId: "acme" }); ``` ## Related * [Quickstart](/quickstart) * [Function Registry](/integrations/function-registry) * [Python SDK](/sdk/python) * [TypeScript SDK](/sdk/typescript) # MCP Server Source: https://docs.raysurfer.com/integrations/mcp Give Claude direct access to your code cache via Model Context Protocol The Raysurfer MCP server exposes code caching as native tools inside Claude Code, Claude Desktop, Cursor, VS Code, Windsurf, and any MCP-compatible client. No install required — runs via `npx`. Source: [rayxc-org/raysurfer-code-caching-mcp](https://github.com/rayxc-org/raysurfer-code-caching-mcp) ## Setup Get your API key from the [dashboard](https://www.raysurfer.com/dashboard/api-keys). ### Claude Code ```bash theme={null} claude mcp add raysurfer -e RAYSURFER_API_KEY=YOUR_API_KEY -- npx -y raysurfer-code-caching-mcp ``` Add `--scope user` to make it available across all projects: ```bash theme={null} claude mcp add raysurfer --scope user -e RAYSURFER_API_KEY=YOUR_API_KEY -- npx -y raysurfer-code-caching-mcp ``` ### Claude Desktop Add to your `claude_desktop_config.json`: * macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` * Windows: `%APPDATA%\Claude\claude_desktop_config.json` ```json theme={null} { "mcpServers": { "raysurfer": { "command": "npx", "args": ["-y", "raysurfer-code-caching-mcp"], "env": { "RAYSURFER_API_KEY": "YOUR_API_KEY" } } } } ``` ### Cursor Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project): ```json theme={null} { "mcpServers": { "raysurfer": { "command": "npx", "args": ["-y", "raysurfer-code-caching-mcp"], "env": { "RAYSURFER_API_KEY": "YOUR_API_KEY" } } } } ``` ### VS Code Add to `.vscode/mcp.json`: ```json theme={null} { "inputs": [ { "password": true, "id": "raysurfer-api-key", "type": "promptString", "description": "Raysurfer API Key" } ], "servers": { "raysurfer": { "command": "npx", "args": ["-y", "raysurfer-code-caching-mcp"], "env": { "RAYSURFER_API_KEY": "${input:raysurfer-api-key}" } } } } ``` ### Windsurf Add to `~/.codeium/windsurf/mcp_config.json`: ```json theme={null} { "mcpServers": { "raysurfer": { "command": "npx", "args": ["-y", "raysurfer-code-caching-mcp"], "env": { "RAYSURFER_API_KEY": "YOUR_API_KEY" } } } } ``` ### Project-Level (.mcp.json) Add to your project root to share with your team: ```json theme={null} { "mcpServers": { "raysurfer": { "command": "npx", "args": ["-y", "raysurfer-code-caching-mcp"], "env": { "RAYSURFER_API_KEY": "${RAYSURFER_API_KEY}" } } } } ``` ## Verify Installation For Claude Code, check the server is running: ``` claude mcp list ``` You should see `raysurfer` listed. Then ask Claude: > Search the Raysurfer cache for code that generates a quarterly report. Claude will call `raysurfer_search` and show you the results. ## Tools The server exposes 4 tools that Claude can call directly: ### raysurfer\_search Search for cached code matching a task description. | Parameter | Type | Required | Default | Description | | -------------- | ------- | -------- | ------- | -------------------------------------------- | | `task` | string | Yes | — | Natural language task description | | `top_k` | number | No | 5 | Maximum results (1-100) | | `min_score` | number | No | 0.3 | Minimum verdict score (0-1) | | `public_snips` | boolean | No | false | Include community public snippets in results | ### raysurfer\_upload Upload code from a successful execution. | Parameter | Type | Required | Default | Description | | ----------- | ------- | -------- | ------- | ----------------------------------- | | `task` | string | Yes | — | What the code accomplishes | | `file` | object | Yes | — | File with `path` and `content` keys | | `succeeded` | boolean | No | true | Whether the execution succeeded | ### raysurfer\_vote Vote on cached code usefulness. | Parameter | Type | Required | Default | Description | | --------------- | ------- | -------- | ------- | --------------------------------- | | `code_block_id` | string | Yes | — | ID of the code block | | `up` | boolean | No | true | Upvote (true) or downvote (false) | | `task` | string | No | — | Original task for context | ### raysurfer\_patterns Get proven task-to-code mappings. | Parameter | Type | Required | Default | Description | | --------- | ------ | -------- | ------- | ----------------------- | | `task` | string | No | — | Filter patterns by task | | `top_k` | number | No | 10 | Maximum results | ## Resources | URI | Description | | -------------------- | ---------------------------------------------- | | `raysurfer://help` | Help text about tools and recommended workflow | | `raysurfer://status` | Connection status and API reachability | # Integrations Source: https://docs.raysurfer.com/integrations/overview All the ways you can use Raysurfer Every integration uses the same two-call pattern — `search()` before your run, `upload()` after — and connects to the same backend. All integrations require a `RAYSURFER_API_KEY`. Get yours from the [dashboard](https://www.raysurfer.com/dashboard/api-keys). ## SDKs * [**Python SDK**](/sdk/python) — `pip install raysurfer`. Drop-in replacement for Claude Agent SDK, or use `search()` / `upload()` directly. * [**TypeScript SDK**](/sdk/typescript) — `npm install raysurfer`. Same API surface as the Python SDK. ## Wrap Any LLM Flow * [**Wrap Any LLM Tool Flow**](/integrations/llm-tool-flows) — Add `search()` and `upload()` around Anthropic, LangChain, or any custom tool-calling loop. * [**Browser Agent**](/integrations/browser-agent) — Cache and reuse Playwright scripts across browser automation runs. ## Claude Code * [**Claude Code Plugin**](/integrations/plugin) — One-command install that bundles the skill, hooks, and MCP server together. * [**Claude Code Skill**](/integrations/skill) — SKILL.md that teaches Claude to check the cache before coding and upload results after. * [**MCP Server**](/integrations/mcp) — Model Context Protocol server that gives Claude direct access to your code cache as native tools. * [**CLI**](/integrations/cli) — Standalone command-line tool for searching, uploading, and voting on cached code. ## Programmatic Tool Calling * [**Function Registry**](/integrations/function-registry) — Publish a registry of your functions so agents can discover and call them across sessions. * [**Register Python Functions**](/integrations/register-python-functions) — Expose Python functions so agents can call them in a remote sandbox. * [**Register TypeScript Functions**](/integrations/register-ts-functions) — Expose TypeScript functions so agents can call them in a remote sandbox. ## API Reference * [**REST API**](/api-reference) — Direct HTTP endpoints. * [**cURL Examples**](/sdk/curl) — Copy-paste cURL commands. Book a 15-minute call and we'll help you pick the right one for your stack. # Claude Code Plugin Source: https://docs.raysurfer.com/integrations/plugin One-command install that bundles the skill, hooks, and MCP server The Raysurfer plugin is the easiest way to get full code caching integration in Claude Code. It bundles the skill, lifecycle hooks, and MCP server configuration into a single installable package. Source: [rayxc-org/raysurfer-code-caching-tool](https://github.com/rayxc-org/raysurfer-code-caching-tool) ## Installation ```bash theme={null} claude plugin add https://github.com/rayxc-org/raysurfer-code-caching-tool ``` Set your API key: ```bash theme={null} export RAYSURFER_API_KEY=your_key_here ``` That's it. The plugin automatically: * Teaches Claude to check the cache before generating code (via the bundled skill) * Runs a hook after Claude finishes responding (via lifecycle hooks) * Configures the MCP server for direct tool access (via `.mcp.json`) ## What's Inside ``` raysurfer-code-caching-tool/ ├── .claude-plugin/ │ └── plugin.json # Plugin manifest ├── skills/ │ └── raysurfer/ │ ├── SKILL.md # Auto-invoked caching skill │ └── references/ │ └── api-reference.md # API docs for Claude ├── hooks/ │ └── hooks.json # Stop hook for upload ├── scripts/ │ ├── check-cache.sh # Cache lookup helper │ └── upload-result.sh # Upload trigger ├── .mcp.json # MCP server config └── CLAUDE.md # Plugin context ``` ### Skill The bundled `/raysurfer` skill auto-invokes when Claude detects coding tasks. It searches the cache before generating code and uploads successful results afterward. See [Skill docs](/integrations/skill) for details. ### Hooks The plugin registers a `Stop` hook that fires after Claude finishes responding. This triggers a lightweight script that logs the event for observability. ### MCP Server The `.mcp.json` configures the `raysurfer-code-caching-mcp` package, giving Claude direct tool access to `raysurfer_search`, `raysurfer_upload`, `raysurfer_vote`, and `raysurfer_patterns`. See [MCP docs](/integrations/mcp) for details. ## Verify After installing, check the plugin is active: ``` /raysurfer search for code that builds a dashboard ``` Claude should search the Raysurfer cache and report results. You can also check MCP status: ``` /mcp ``` ## Plugin vs Individual Components | Approach | Pros | Cons | | ---------------------- | --------------------------------------- | ---------------------------------- | | **Plugin** (this page) | One-command install, everything bundled | Requires Claude Code | | **Skill** alone | Lightweight, no npm dependency | No MCP tools, no hooks | | **MCP** alone | Direct tool access for Claude | No auto-invoke workflow | | **CLI** alone | Works everywhere, scriptable | Manual only, no Claude integration | For most users, the plugin is the right choice. Use individual components only if you need fine-grained control or work outside Claude Code. # Register Python Functions Source: https://docs.raysurfer.com/integrations/register-python-functions Expose your existing Python functions so agents can call them in a remote sandbox Register your existing Python functions so agents can call them. Raysurfer wraps your functions with `@rs.tool`, auto-infers the schema from the signature and docstring, and routes tool calls back to your app during sandbox execution. Source: [rayxc-org/raysurfer-python](https://github.com/rayxc-org/raysurfer-python) ## Setup Install the SDK and set `RAYSURFER_API_KEY` in your environment. ```bash theme={null} uv add raysurfer ``` ## Register Tools `@rs.tool` can wrap any Python function. The function signature is converted to schema fields, and the function docstring is used as the tool description in the tool payload. ```python theme={null} from raysurfer import AsyncRaySurfer rs = AsyncRaySurfer(api_key="your_api_key") @rs.tool def add(a: int, b: int) -> int: """Add two numbers.""" return a + b ``` ## How Functions Get Passed Back To The Agent `@rs.tool` registers two things locally in your app process: * A tool schema (name, description, parameters) that Raysurfer sends to the sandbox run * A callback function that stays local and is invoked when the sandbox requests that tool Execution flow: 1. `execute()` opens a callback session and sends tool schemas plus your task to Raysurfer. 2. Sandbox code calls a tool (for example `add(5, 3)`). 3. Raysurfer sends a `tool_call` message back to your app. 4. Your local callback runs, and the SDK sends `tool_result` back to the sandbox. 5. The sandbox continues running with that result. This is the mechanism that passes tool execution back into your running agent app. ## Execute Modes Use exactly one mode per `execute()` call: ### Primary Mode: Pass `user_code` ```python theme={null} result = await rs.execute( "Compute 5 + 3", user_code="print(add(5, 3))", ) ``` ### End-To-End Sandbox Example ```python theme={null} import asyncio from raysurfer import AsyncRaySurfer async def main() -> None: rs = AsyncRaySurfer(api_key="your_api_key") @rs.tool def add(a: int, b: int) -> int: """Add two numbers.""" return a + b result = await rs.execute( "Compute 5 + 3", user_code="result = add(5, 3)\nprint(result)", ) print(result.result) # "8" print(result.tool_calls) # tool call history asyncio.run(main()) ``` ### Optional Mode: Sandbox Codegen In this mode, code is generated inside Raysurfer's remote sandbox from your prompt. ```python theme={null} result = await rs.execute( "Compute 5 + 3", codegen_api_key="YOUR_ANTHROPIC_API_KEY", codegen_prompt="Write Python code that calls add(a, b) for 5 and 3, then prints the result.", codegen_model="claude-opus-4-6", ) ``` ## Full SDK Reference See [Python SDK](/sdk/python#programmatic-tool-calling) for full method signatures and response fields. # Register TypeScript Functions Source: https://docs.raysurfer.com/integrations/register-ts-functions Expose your existing TypeScript functions so agents can call them in a remote sandbox Register your existing TypeScript functions so agents can call them. Raysurfer wraps your callbacks with `tool(...)`, and routes tool calls back to your app during sandbox execution. Source: [rayxc-org/raysurfer-ts](https://github.com/rayxc-org/raysurfer-ts) ## Setup Install the SDK and set `RAYSURFER_API_KEY` in your environment. ```bash theme={null} bun add raysurfer ``` ## Register Tools `tool(name, description, parameters, callback)` can wrap any local callback you want to expose. The `description` is included in the tool payload. ```typescript theme={null} import { RaySurfer } from "raysurfer"; const rs = new RaySurfer({ apiKey: "your_api_key" }); rs.tool("add", "Add two numbers", { a: "integer", b: "integer" }, async (args) => { return String(Number(args.a) + Number(args.b)); }); ``` ## How Functions Get Passed Back To The Agent `tool(...)` registers two things locally in your app process: * A tool schema (name, description, parameters) that Raysurfer sends to the sandbox run * A callback function that stays local and is invoked when the sandbox requests that tool Execution flow: 1. `execute()` opens a callback session and sends tool schemas plus your task to Raysurfer. 2. Sandbox code calls a tool (for example `add(5, 3)`). 3. Raysurfer sends a `tool_call` message back to your app. 4. Your local callback runs, and the SDK sends `tool_result` back to the sandbox. 5. The sandbox continues running with that result. This is the mechanism that passes tool execution back into your running agent app. ## Execute Modes Use exactly one mode per `execute()` call: ### Primary Mode: Pass `userCode` ```typescript theme={null} const result = await rs.execute("Compute 5 + 3", { userCode: "print(add(5, 3))", }); ``` ### End-To-End Sandbox Example ```typescript theme={null} import { RaySurfer } from "raysurfer"; const rs = new RaySurfer({ apiKey: "your_api_key" }); rs.tool("add", "Add two numbers", { a: "integer", b: "integer" }, async (args) => { return String(Number(args.a) + Number(args.b)); }); const result = await rs.execute("Compute 5 + 3", { userCode: "result = add(5, 3)\nprint(result)", }); console.log(result.result); // "8" console.log(result.toolCalls); // tool call history ``` ### Optional Mode: Sandbox Codegen In this mode, code is generated inside Raysurfer's remote sandbox from your prompt. ```typescript theme={null} const result = await rs.execute("Compute 5 + 3", { codegen: { provider: "anthropic", apiKey: "YOUR_ANTHROPIC_API_KEY", model: "claude-opus-4-6", prompt: "Write Python code that calls add(a, b) for 5 and 3, then prints the result.", }, }); ``` ## Full SDK Reference See [TypeScript SDK](/sdk/typescript#programmatic-tool-calling) for full method signatures and response fields. # Claude Code Skill Source: https://docs.raysurfer.com/integrations/skill Teach Claude to automatically check the cache before coding and upload results after The Raysurfer skill is a `SKILL.md` file that teaches Claude Code to automatically use the code cache. Claude checks for cached code before generating new code and uploads successful results for future reuse. Source: [rayxc-org/raysurfer-code-caching-skill](https://github.com/rayxc-org/raysurfer-code-caching-skill) ## Installation Clone the skill into your Claude Code skills directory: ```bash Personal (all projects) theme={null} git clone https://github.com/rayxc-org/raysurfer-code-caching-skill.git \ ~/.claude/skills/raysurfer ``` ```bash Project-level theme={null} git clone https://github.com/rayxc-org/raysurfer-code-caching-skill.git \ .claude/skills/raysurfer ``` Set your API key: ```bash theme={null} export RAYSURFER_API_KEY=your_key_here ``` ## How It Works Once installed, the skill auto-invokes when Claude detects a coding task. The workflow: Before generating code, Claude calls the Raysurfer API to check if similar code already exists. If a high-quality match is found (score >= 0.7, positive votes), Claude uses the cached code directly. Otherwise, it generates new code normally. After using cached code, Claude upvotes it if it worked or downvotes it if it had issues. After successfully generating new code, Claude uploads it to the cache for future reuse. ## Manual Invocation You can also invoke the skill explicitly: ``` /raysurfer Generate a quarterly revenue report ``` Or trigger an upload: ``` /raysurfer upload ``` ## What the Skill Contains ``` raysurfer-code-caching-skill/ ├── SKILL.md # Instructions Claude follows └── references/ └── api-reference.md # API endpoint docs for Claude ``` The skill uses `curl` commands to call the API directly, so it works without the CLI or MCP server installed. It only needs `RAYSURFER_API_KEY` set in your environment. ## Customization The skill is a plain markdown file. Fork the repo and edit `SKILL.md` to: * Adjust the cache hit threshold (default: `combined_score >= 0.7`) * Change which tools Claude is allowed to use * Add project-specific caching rules * Modify the upload criteria # Is Raysurfer Right for You? Source: https://docs.raysurfer.com/is-it-right-for-me A quick guide to help you decide if Raysurfer fits your agent workflow ## The Quick Test Ask yourself two questions: 1. **How many tool calls does your agent make per run?** 2. **Does your agent already generate and execute code in production?** If the answer is "2-3 tools, under 2 seconds" — Raysurfer isn't for you yet. Your agent is simple enough that caching won't move the needle. If the answer is "10-30+ tools, 30+ seconds, and yes we run generated code" — keep reading. ## Where Raysurfer Works Raysurfer works when your agent chains many tool calls together and the same patterns repeat across runs. ### Freight Logistics A logistics agent processing a shipment reads a database row, pulls a column, checks a Slack channel, cross-references an email, verifies a date — that's 10 tools before it even has context. Then it emails carriers, reconciles responses, and books shipments. Total: 30-50 tool calls per run. The key insight: **the variance per customer is low.** The same customer ships similar freight the same way every time. The agent should be running the same proven workflow, not regenerating 44 tool calls from scratch. ### Real Estate Search A real estate agent pulls 30 tools per house search — neighborhood data, comparable prices, market trends, school ratings, commute times. But each home searcher is typically looking for one type of house in one neighborhood. The tool call pattern is nearly identical across searches for the same buyer. ### The Pattern Any workflow where: * Your agent chains **10+ tool calls** per run * Similar requests produce **similar tool call sequences** * The same patterns repeat across customers or sessions ## Good Fit Checklist Your agent runs 10-50+ tool calls per task, chaining outputs from one into the next Similar requests come in repeatedly — same customer types, same operations, same data shapes Getting consistent, correct results matters. You can't afford 60% reliability on a \$50k contract. Your agent generates and executes code in production, not just calling tools via JSON ### The compounding error problem This is why long chains need caching. If each tool call in a chain has a 98% success rate: | Tool Calls | End-to-End Success Rate | | ---------- | ----------------------- | | 5 calls | 90% | | 10 calls | 82% | | 20 calls | 67% | | 30 calls | 55% | | 50 calls | 36% | With Raysurfer, the agent retrieves the entire proven chain as one code block. No per-step failure risk — same correct result every time. ### What a good workflow looks like The ideal Raysurfer pattern is a multi-step workflow where each step depends on the previous step's output: ```python theme={null} llm_input = params out1 = query_customer_records_from_postgres_database(llm_input) out2 = lookup_contact_details_in_local_csv_file(out1[2]) out3 = fetch_purchase_history_from_stripe_api(out2.files[-1]) out4 = calculate_loyalty_tier_from_transaction_count(out3.length) return out4 ``` On the **first run**, your agent generates this code, executes it, and Raysurfer caches the entire block. On the **second run**, Raysurfer retrieves the proven code and runs it directly — no LLM regeneration, no trial-and-error, no compounding errors. ## Not the Right Fit (Yet) Raysurfer amplifies what your agent already does well. If your agent isn't working yet, caching won't fix it. **Your agent makes 1-3 tool calls per run.** The overhead of searching the cache outweighs the benefit. Caching helps when the alternative is 30+ serial LLM roundtrips. **Every request is completely unique.** If no two requests produce similar tool call sequences, there's nothing useful to cache. **Your agent doesn't generate code yet.** Raysurfer caches generated code blocks, not raw LLM text or JSON tool responses. If your agent is still in the serial tool-calling paradigm (send JSON to tool, get JSON back, feed to LLM, repeat), you need to migrate to code generation first. **Your agent's base accuracy is below 40%.** If the agent is producing garbage, caching garbage doesn't help — it feeds the noise. Get your agent working reliably first, then add caching to make it consistent. ## How to Tell It's Working Once you integrate Raysurfer: 1. **First run** takes normal time (agent generates code from scratch) 2. **Second run** with a similar query returns near-instantly (proven code retrieved) 3. **Accuracy improves over time** as high-reputation code gets prioritized and low-quality code gets deprioritized The highest-value signal: your agent produces the **same correct output** for similar requests, every time, instead of rolling the dice on fresh generation. Follow the quickstart to integrate Raysurfer in under 5 minutes Book a 15-minute call and we'll evaluate your use case together. # Per-Function Reputation Source: https://docs.raysurfer.com/per-function-reputation Track reputation and telemetry for each function inside cached code Per-function reputation helps agents improve the weak parts of a cached snippet, not just the snippet as a whole. When enabled: * Upload extracts function fingerprints from your code and tracks function-level votes. * Search injects function diagnostics into source comments. * Search responses include `functions` metadata with telemetry and vote stats. ## Quickstart ### 1) Log function outputs Call `log(...)` inside functions you want tracked. ```python Python theme={null} from raysurfer import log def extract_contacts(html: str) -> list[str]: contacts = [] # parse html... log(contacts) return contacts ``` ```typescript TypeScript theme={null} import { log } from "raysurfer"; function extractContacts(html: string): string[] { const contacts: string[] = []; // parse html... log(contacts); return contacts; } ``` ### 2) Upload with per-function tracking ```python Python theme={null} from raysurfer.types import FileWritten client.upload( task="Extract contacts from company pages", file_written=FileWritten(path="contacts.py", content=code), succeeded=True, execution_logs=logs, per_function_reputation=True, ) ``` ```typescript TypeScript theme={null} await client.upload({ task: "Extract contacts from company pages", fileWritten: { path: "contacts.ts", content: code }, succeeded: true, executionLogs: logs, perFunctionReputation: true, }); ``` ### 3) Search with per-function diagnostics ```python Python theme={null} result = client.search( task="Extract contacts from company pages", per_function_reputation=True, ) for fn in result.matches[0].functions or []: print(fn.function_name, fn.thumbs_up, fn.thumbs_down, fn.total_logged_calls) ``` ```typescript TypeScript theme={null} const result = await client.search({ task: "Extract contacts from company pages", perFunctionReputation: true, }); for (const fn of result.matches[0].functions ?? []) { console.log(fn.functionName, fn.thumbsUp, fn.thumbsDown, fn.totalLoggedCalls); } ``` ## What You Get * Inline comments like `# [raysurfer ab12cd34] ...` inside matched source. * A `functions` array on each match with: * vote counts (`thumbs_up`/`thumbs_down`) * telemetry (`total_logged_calls`, `output_completeness`, `avg_output_size`) * status timestamps (`last_success_at`, `last_failure_at`) The injected comments include a history URL for each function: `https://api.raysurfer.com/api/functions/.md` Use this for drill-down diagnostics before rewriting a weak function. ## Defaults * Low-level `search()` / `upload()` APIs: per-function reputation is opt-in. * `get_code_files()` / `getCodeFiles()` wrappers: per-function reputation is enabled by default. # Public Code Snippets Source: https://docs.raysurfer.com/public-snippets Browse and contribute verified code snippets to the community registry ## Browse Snippets Explore the public snippet registry at [raysurfer.com/public\_snippets](https://www.raysurfer.com/public_snippets) — search by language, sort by votes, and find proven code for common tasks. ## Contribute a Snippet Upload a snippet to the public registry using the SDK. Requires an API key from the [dashboard](https://www.raysurfer.com/dashboard/api-keys). ```python Python theme={null} from raysurfer import AsyncRaySurfer from raysurfer.types import FileWritten rs = AsyncRaySurfer() await rs.upload( task="Send an HTML email with SES", file_written=FileWritten( path="ses_send_email.py", content=open("ses_send_email.py").read(), ), succeeded=True, public=True, ) ``` ```typescript TypeScript theme={null} import { RaySurfer } from "raysurfer"; import { readFileSync } from "fs"; const rs = new RaySurfer(); await rs.upload({ task: "Send an HTML email with SES", fileWritten: { path: "ses_send_email.ts", content: readFileSync("ses_send_email.ts", "utf-8"), }, succeeded: true, public: true, }); ``` ## What Makes a Good Public Snippet * **Parameterized inputs** — no hardcoded API keys, file paths, or user-specific values * **Self-contained** — works with standard dependencies, no internal imports * **Descriptive task** — the `task` string should clearly describe what the code does * **Tested** — set `succeeded=True` only if the code actually ran successfully ## Quality and Voting Public snippets are ranked by community votes (thumbs up / thumbs down). Higher-voted snippets surface first in search results and are more likely to be reused by other agents. Book a 15-minute call to discuss bulk uploads or registry partnerships. # Quickstart Source: https://docs.raysurfer.com/quickstart Get started with Raysurfer in under 5 minutes Raysurfer is designed for agents that chain multiple tool calls together in production workflows. If your agent only makes 1-3 tool calls per run, check [Is Raysurfer Right for You?](/is-it-right-for-me) first. ## Installation ```bash Python theme={null} uv add raysurfer ``` ```bash TypeScript theme={null} bun add raysurfer ``` ## Set Your API Key Get your API key from the [dashboard](https://www.raysurfer.com/dashboard/api-keys) and set it as an environment variable: ```bash theme={null} export RAYSURFER_API_KEY=your_api_key_here ``` ## Drop-in Replacement Raysurfer wraps the Claude Agent SDK. Swap your client and method names: ```python Python theme={null} # Before from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions async with ClaudeSDKClient(options) as client: await client.query("Generate quarterly report") async for msg in client.receive_response(): print(msg) # After from raysurfer import RaysurferClient from claude_agent_sdk import ClaudeAgentOptions # Options come from Claude SDK async with RaysurferClient(options) as client: await client.query("Generate quarterly report") async for msg in client.response(): print(msg) ``` ```typescript TypeScript theme={null} // Before import { ClaudeSDKClient } from "@anthropic-ai/claude-agent-sdk"; const client = new ClaudeSDKClient(options); for await (const msg of client.query("Generate quarterly report")) { console.log(msg); } // After import { RaysurferClient } from "raysurfer"; const client = new RaysurferClient(options); for await (const msg of client.query("Generate quarterly report")) { console.log(msg); } ``` ## Key Differences | Claude SDK | Raysurfer | | --------------------------- | ------------------- | | `ClaudeSDKClient` | `RaysurferClient` | | `client.query()` | `client.query()` | | `client.receive_response()` | `client.response()` | Options (`ClaudeAgentOptions`) come directly from `claude_agent_sdk` — no Raysurfer-specific options needed. ## Verification Checklist Follow these three steps to confirm caching is working and understand the similarity threshold for your use case. ### Step 1: Confirm Code Generation on First Run Run your agent with a unique query for the first time. This run generates code from scratch via the Claude Agent SDK, so it will take the full generation time. ```python theme={null} await client.query("Generate a quarterly revenue report for Q3 2025") ``` Watch the output — you should see the agent generating code, executing tools, and producing results at normal speed. This first run populates the cache. ### Step 2: Confirm Cache Hit on Second Run Run the **exact same query** again: ```python theme={null} await client.query("Generate a quarterly revenue report for Q3 2025") ``` This time, Raysurfer pulls the cached code file instead of regenerating it. You should see the result return near-instantly because the only synchronous work is fetching the cached code — everything else runs asynchronously. If the second run completes dramatically faster than the first, caching is working. You're in the clear. Run the same query using `claude-agent-sdk` directly (without Raysurfer) to get a baseline time, then compare it against the cached Raysurfer run to measure your exact speedup. ### Step 3: Explore the Similarity Boundary Now try **similar but not identical** queries to see how far you can drift before you stop getting cache hits. Start close and gradually move further away: ```python theme={null} # Very similar — likely cache hit await client.query("Generate a quarterly revenue report for Q4 2025") # Moderately similar — may or may not hit cache await client.query("Generate a monthly revenue summary for Q3 2025") # Different intent — likely cache miss, generates fresh code await client.query("Build a customer churn analysis dashboard") ``` For each query, note whether the response time is near-instant (cache hit) or takes the full generation time (cache miss). This tells you how semantically similar your queries need to be to reuse the same cached workflow code. Book a 15-minute call and we'll walk through your integration together. ## What Gets Cached? Raysurfer caches: * Code outputs from agent tool calls * Generated reports, templates, and documents * API response patterns * Any structured output your agent produces Raysurfer automatically verifies and stores successful code snippets from your runs for future reuse. ## Programmatic Tool Calling Register tools, then either pass in code (primary mode) or optionally generate code in the sandbox with your own key + prompt: ```python Python theme={null} from raysurfer import AsyncRaySurfer rs = AsyncRaySurfer(api_key="your_api_key") @rs.tool def add(a: int, b: int) -> int: """Add two numbers together.""" return a + b user_code = "result = add(5, 3)\nprint(result)" result = await rs.execute( "Use add to compute 5 + 3, then print the result", user_code=user_code, ) print(result.result) # "8" print(result.cache_hit) # False (reserved field) ``` ```typescript TypeScript theme={null} import { RaySurfer } from "raysurfer"; const rs = new RaySurfer({ apiKey: "your_api_key" }); rs.tool("add", "Add two numbers", { a: "integer", b: "integer" }, async (args) => String(Number(args.a) + Number(args.b)) ); const userCode = ` result = add(5, 3) print(result) `; const result = await rs.execute("Use add to compute 5 + 3, then print the result", { userCode, }); console.log(result.result); // "8" console.log(result.cacheHit); // false (reserved field) ``` See [Python SDK — Programmatic Tool Calling](/sdk/python#programmatic-tool-calling) or [TypeScript SDK — Programmatic Tool Calling](/sdk/typescript#programmatic-tool-calling) for full details. In Python, `@rs.tool` can wrap any function and uses its docstring as the tool description in the schema payload. In TypeScript, `tool(name, description, parameters, callback)` wraps any callback and uses your `description` string the same way. `execute()` supports two modes: `user_code`/`userCode` (primary) or optional sandbox codegen with a user-provided prompt and provider key. If you already have an LLM tool loop and just want to wrap it with `.search()` + `.upload()`, use [Wrap Any LLM Tool Flow](/integrations/llm-tool-flows). ## Next Steps Full Python SDK reference and examples Full TypeScript SDK reference and examples Learn about proven code retrieval and reputation scoring Check if your agent workflow is a good fit for Raysurfer # Rate Limits & Pricing Source: https://docs.raysurfer.com/rate-limits Plans, pricing, request quotas, and rate limits ## Pricing | | **Free** | **Pro** | **Enterprise** | | ------------------------------- | ----------- | ----------------------------------- | ------------------------------------------------------ | | **Price** | \$0 | \$28/month | Custom — [contact sales](mailto:raymond@raysurfer.com) | | **Namespace** | Single user | Dedicated (per org + per workspace) | Dedicated (per org + per workspace) | | **Max members** | 1 | 10 | 100 | | **Max workspaces** | 0 | 10 | 50 | | **Stored embeddings** | 500 | Unlimited | Unlimited | | **Self-service key management** | — | — | Yes | All paid plans include priority support and access to the analytics dashboard. Start free, upgrade when you need a private namespace or team access. [See pricing →](https://www.raysurfer.com/pricing) *** ## Request Quotas Each plan tier has a maximum number of API requests per billing period. Once exceeded, the API returns `429 Too Many Requests` until the period resets. | Tier | Quota | Period | Reset | | -------------- | --------------- | ------- | ----------------- | | **Free** | 500 requests | Monthly | 1st of each month | | **Pro** | 10,000 requests | Daily | Midnight UTC | | **Enterprise** | Custom | Custom | Custom | ### Quota Exceeded Response When you exceed your quota, the API returns: ```json theme={null} { "error": "quota_exceeded", "message": "You have used 500/500 requests this month. Upgrade your plan for higher limits.", "tier": "free", "limit": 500, "period": "monthly", "usage": 500 } ``` ## HTTP Rate Limits Independent of quotas, all tiers share per-minute rate limits on API endpoints. These prevent burst abuse and ensure fair access. | Endpoint Category | Limit | Endpoints | | ----------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Retrieve** | 200/minute | `/api/retrieve/code-blocks`, `/api/retrieve/best-for-task`, `/api/retrieve/executions`, `/api/retrieve/few-shot-examples`, `/api/retrieve/task-patterns`, `/api/retrieve/code-files` | | **Store** | 30/minute | `/api/store/code-block`, `/api/store/execution`, `/api/store/execution-result`, `/api/store/bulk-execution-result`, `/api/store/auto-review`, `/api/store/cache-usage`, `/api/store/synthetic-queries` | | **Default** | 100/minute | All other endpoints | ## Plan Limits Each tier also sets limits on organization resources. | Tier | Max Members | Max Workspaces | Namespace | | -------------- | ----------- | -------------- | ----------------------------------- | | **Free** | 1 | 0 | Shared | | **Pro** | 10 | 10 | Dedicated (per org + per workspace) | | **Enterprise** | 100 | 50 | Dedicated (per org + per workspace) | ## Handling Rate Limits Both SDKs include built-in retry logic with exponential backoff. When a `429` response is received, the SDK automatically retries up to 3 times before raising an error. If all retries are exhausted, a `RateLimitError` is raised: ```python Python theme={null} from raysurfer import RaySurfer from raysurfer.exceptions import RateLimitError client = RaySurfer(api_key="your_api_key") try: result = client.search(task="Fetch GitHub repos") except RateLimitError as e: print(f"Rate limited after retries: {e}") if e.retry_after: print(f"Try again in {e.retry_after}s") ``` ```typescript TypeScript theme={null} import { RaySurfer, RateLimitError } from "raysurfer"; const client = new RaySurfer({ apiKey: "your_api_key" }); try { const result = await client.search({ task: "Fetch GitHub repos" }); } catch (e) { if (e instanceof RateLimitError) { console.log(`Rate limited after retries: ${e.message}`); if (e.retryAfter) { console.log(`Try again in ${e.retryAfter}ms`); } } } ``` Need higher limits? [Contact us](mailto:raymond@raysurfer.com) to discuss Enterprise plans with custom quotas and self-service key management. # Results Source: https://docs.raysurfer.com/results Real-world results showing Raysurfer agents that are more consistent, faster, and cheaper on similar tasks # Real-World Results ## Customer Results ### Cartage: 70% to 95% Accuracy Cartage integrated Raysurfer into their production agent workflow and saw accuracy improve from 70% to 95% on repetitive multi-step tasks. By retrieving proven code instead of regenerating from scratch each run, their agent produced consistent, correct results — even on complex tool chains that previously failed intermittently. ## Benchmark Results Side-by-side runs where baseline and Raysurfer modes are evaluated on the same task sets with the same budgets. Compared modes: * `claude-agent-sdk` baseline * Raysurfer reuse mode On similar tasks, Raysurfer finishes more work with fewer LLM interaction calls and less elapsed time. ### Headline Numbers (February 20, 2026) | Run Set | Tasks | Baseline Consistency | Raysurfer Consistency | Baseline Interaction Calls | Raysurfer Interaction Calls | Baseline Total Time | Raysurfer Total Time | | ------------------------------------------------- | ----: | -------------------: | --------------------: | -------------------------: | --------------------------: | ------------------: | -------------------: | | Public one-shot implementation tasks | 20 | 5.0% (1/20) | 100.0% (20/20) | 81 total (4.05/attempt) | 0 total (0.00/attempt) | 860.9s | 14.7s | | Existing benchmark tasks (10 HumanEval + 10 MBPP) | 20 | 0.0% (0/20) | 100.0% (20/20) | 68 total (3.40/attempt) | 20 total (1.00/attempt) | 291.3s | 0.436s | ### What This Means 1. **Consistent** — 100% consistency on cached tasks vs 0-5% without caching 2. **Faster** — seconds instead of minutes for the same workloads 3. **Cheaper** — fewer interaction calls means less model/tool loop work per attempt ## Methodology 1. Use the same task list for baseline and Raysurfer runs. 2. Keep model, turn limits, and timeout budgets fixed between modes. 3. Seed Raysurfer with verified snippets before the Raysurfer run. 4. Record per-attempt completion, elapsed seconds, and interaction-call metric. 5. Score consistency as `completed_within_180_seconds / total_attempts`. ## Interaction-Call Metric * In `examples/raysurfer-public-oneshot-eval`, calls come from `tools=` in run details (`run_agent_eval.py`). * In `examples/raysurfer-existing-benchmarks-eval`, calls come from `metric=` in run details (`run_benchmark_eval.py`): baseline uses Claude tool-loop calls, Raysurfer uses retrieved candidates evaluated. ## Re-run Commands ### Public one-shot benchmark ```bash theme={null} cd examples/raysurfer-public-oneshot-eval PYTHONPATH=../../raysurfer-python/src uv run python scripts/seed_verified_snippets.py --tasks tasks/tasks.json uv run python scripts/run_agent_eval.py --tasks tasks/tasks.json --mode baseline --out runs/baseline.json --model haiku --max-turns 4 --timeout-seconds 180 RAYSURFER_BASE_URL=http://127.0.0.1:8000 RAYSURFER_API_KEY=your_key uv run python scripts/run_agent_eval.py --tasks tasks/tasks.json --mode raysurfer --out runs/with_raysurfer.json --model haiku --max-turns 4 --timeout-seconds 180 uv run python scripts/score_eval.py --tasks tasks/tasks.json --baseline-runs runs/baseline.json --raysurfer-runs runs/with_raysurfer.json --json-out runs/summary.json ``` ### Existing benchmark ```bash theme={null} cd examples/raysurfer-existing-benchmarks-eval uv run python scripts/build_tasks.py --out tasks/existing_benchmarks_20.json --humaneval-limit 10 --mbpp-limit 10 PYTHONPATH=../../raysurfer-python/src uv run python scripts/seed_reference_solutions.py --tasks tasks/existing_benchmarks_20.json uv run python scripts/run_benchmark_eval.py --tasks tasks/existing_benchmarks_20.json --mode baseline --out runs/baseline.json --model haiku --max-turns 4 --timeout-seconds 180 --validation-timeout-seconds 20 --raysurfer-source reference uv run python scripts/run_benchmark_eval.py --tasks tasks/existing_benchmarks_20.json --mode raysurfer --out runs/with_raysurfer.json --model haiku --max-turns 4 --timeout-seconds 180 --validation-timeout-seconds 20 --raysurfer-source reference uv run python scripts/score_eval.py --tasks tasks/existing_benchmarks_20.json --baseline-runs runs/baseline.json --raysurfer-runs runs/with_raysurfer.json --json-out runs/summary.json ``` ## Artifacts * `examples/raysurfer-public-oneshot-eval/runs/baseline.json` * `examples/raysurfer-public-oneshot-eval/runs/with_raysurfer.json` * `examples/raysurfer-public-oneshot-eval/runs/summary.json` * `examples/raysurfer-existing-benchmarks-eval/runs/baseline.json` * `examples/raysurfer-existing-benchmarks-eval/runs/with_raysurfer.json` * `examples/raysurfer-existing-benchmarks-eval/runs/summary.json` # cURL / REST API Source: https://docs.raysurfer.com/sdk/curl Use the Raysurfer API directly with curl or any HTTP client — no SDK required All Raysurfer functionality is available over plain HTTP. Every request uses `POST` with a JSON body (except health check and deletion). ## Authentication Include your API key in the `Authorization` header: ```bash theme={null} -H "Authorization: Bearer $RAYSURFER_API_KEY" ``` ### Optional headers | Header | Description | | --------------------------- | ----------------------------------------------------- | | `X-Raysurfer-Org-Id` | Organization ID (team/enterprise) | | `X-Raysurfer-Workspace-Id` | Workspace ID (enterprise only) | | `X-Raysurfer-Snips-Desired` | `"company"` or `"client"` — scope of private snippets | | `X-Raysurfer-Namespace` | Custom namespace override | *** ## Base URL ``` https://api.raysurfer.com ``` *** ## Retrieve ### Search for code snippets The primary retrieval endpoint. Returns cached code blocks ranked by semantic similarity, community votes, and error resilience. ```bash theme={null} curl -X POST https://api.raysurfer.com/api/retrieve/search \ -H "Authorization: Bearer $RAYSURFER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "task": "Fetch GitHub trending repos", "top_k": 5, "min_verdict_score": 0.3, "prefer_complete": false }' ``` | Field | Type | Default | Description | | ------------------- | ------- | -------- | ---------------------------------- | | `task` | string | required | Natural language task description | | `top_k` | integer | `5` | Max results to return | | `min_verdict_score` | float | `0.3` | Minimum quality threshold (0–1) | | `prefer_complete` | boolean | `false` | Rank longer implementations higher | | `input_schema` | object | `null` | Optional input schema filter | **Response:** ```json theme={null} { "matches": [ { "code_block": { "id": "abc123", "name": "github_fetcher", "description": "Fetches trending repos from GitHub", "source": "import httpx\n\ndef fetch_trending():\n ...", "entrypoint": "fetch_trending", "language": "python", "dependencies": ["httpx"] }, "combined_score": 0.85, "vector_score": 0.82, "verdict_score": 0.90, "error_resilience": 0.75, "thumbs_up": 12, "thumbs_down": 1, "filename": "github_fetcher.py", "language": "python", "entrypoint": "fetch_trending", "dependencies": ["httpx"] } ], "total_found": 3, "cache_hit": false, "search_namespaces": ["code_blocks", "task_patterns"] } ``` ### Get few-shot examples ```bash theme={null} curl -X POST https://api.raysurfer.com/api/retrieve/few-shot-examples \ -H "Authorization: Bearer $RAYSURFER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "task": "Parse CSV files", "k": 3 }' ``` ### Get task patterns ```bash theme={null} curl -X POST https://api.raysurfer.com/api/retrieve/task-patterns \ -H "Authorization: Bearer $RAYSURFER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "task": "API integration", "min_thumbs_up": 5, "top_k": 20 }' ``` ### Query execution history ```bash theme={null} curl -X POST https://api.raysurfer.com/api/retrieve/executions \ -H "Authorization: Bearer $RAYSURFER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code_block_id": "abc123", "limit": 10 }' ``` *** ## Store ### Upload code from an execution (recommended) The simplest way to store code. Send the files your agent wrote and the task it was solving. The backend auto-detects language, entrypoint, and tags, then optionally votes on quality. ```bash theme={null} curl -X POST https://api.raysurfer.com/api/store/execution-result \ -H "Authorization: Bearer $RAYSURFER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "task": "Parse CSV and generate a summary chart", "files_written": [ {"path": "parser.py", "content": "import csv\n\ndef parse(path):\n ..."}, {"path": "chart.py", "content": "import matplotlib.pyplot as plt\n\ndef plot(data):\n ..."} ], "succeeded": true, "use_raysurfer_ai_voting": true, "execution_logs": "Parsed 150 rows, chart saved to output.png" }' ``` | Field | Type | Default | Description | | ------------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- | | `task` | string | required | The task the agent was solving | | `files_written` | array | required | `[{"path": "...", "content": "..."}]` | | `succeeded` | boolean | required | Whether the task completed successfully | | `use_raysurfer_ai_voting` | boolean | `true` | Let Raysurfer AI vote on stored blocks. Ignored when `user_vote` is provided. | | `user_vote` | integer | `null` | User-provided vote: `1` = thumbs up, `-1` = thumbs down. Skips AI voting. | | `vote_source` | string | `null` | `"ai"` or `"human"` — explicitly sets vote source. When omitted, `user_vote` defaults to `"human"`, AI voting defaults to `"ai"`. | | `vote_count` | integer | `1` | Number of votes to apply. Useful for pre-seeding community votes. | | `execution_logs` | string | `null` | Stdout/stderr for vote context | | `run_url` | string | `null` | Link to the finished run log | ### Bulk upload with grading Upload multiple prompts and files. The backend grades each file independently. ```bash theme={null} curl -X POST https://api.raysurfer.com/api/store/bulk-execution-result \ -H "Authorization: Bearer $RAYSURFER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompts": ["Build a CLI tool", "Add CSV export"], "files_written": [ {"path": "cli.py", "content": "import click\n..."}, {"path": "export.py", "content": "import csv\n..."} ], "log_files": [ {"path": "logs/run.log", "content": "Task completed", "encoding": "utf-8"} ], "use_raysurfer_ai_voting": true }' ``` ### Store a code block (advanced) Store a single code block with full metadata. Use this when you need fine-grained control over what's stored. ```bash theme={null} curl -X POST https://api.raysurfer.com/api/store/code-block \ -H "Authorization: Bearer $RAYSURFER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "GitHub User Fetcher", "source": "import httpx\n\ndef fetch_user(username: str) -> dict:\n resp = httpx.get(f\"https://api.github.com/users/{username}\")\n return resp.json()", "entrypoint": "fetch_user", "language": "python", "description": "Fetches user profile data from the GitHub API", "tags": ["github", "api"], "dependencies": ["httpx"], "example_queries": ["get github user info", "fetch github profile"] }' ``` ### Delete a snippet ```bash theme={null} curl -X POST https://api.raysurfer.com/api/snippets/delete \ -H "Authorization: Bearer $RAYSURFER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "snippet_id": "CODE_BLOCK_ID" }' ``` ### Check background task status ```bash theme={null} curl https://api.raysurfer.com/api/store/status/TASK_ID \ -H "Authorization: Bearer $RAYSURFER_API_KEY" ``` *** ## Vote Record whether cached code was useful. This feeds back into the scoring system so better code surfaces higher. ```bash theme={null} curl -X POST https://api.raysurfer.com/api/store/cache-usage \ -H "Authorization: Bearer $RAYSURFER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "task": "Fetch GitHub trending repos", "code_block_id": "abc123", "code_block_name": "github_fetcher", "code_block_description": "Fetches trending repos from GitHub", "succeeded": true }' ``` Voting happens automatically when `use_raysurfer_ai_voting` is `true` (the default) in the store endpoints. You can also provide `user_vote` or `user_votes` to vote yourself — AI voting is skipped when user votes are provided. Use the vote endpoint directly only when you need manual control over cached code blocks. *** ## Health check ```bash theme={null} curl https://api.raysurfer.com/health ``` **Response:** ```json theme={null} {"status": "healthy", "redis": "connected"} ``` *** ## Error responses All errors return a JSON body with a `detail` field: ```json theme={null} {"detail": "Invalid API key"} ``` | Status | Meaning | | ------ | ------------------------------------------------ | | 401 | Invalid or missing API key | | 404 | Resource not found | | 422 | Validation error (check request body) | | 429 | Rate limit exceeded — check `Retry-After` header | | 500 | Server error | *** ## Full example: search then upload A complete workflow — pull cached code, use it, then push results back: ```bash theme={null} # 1. Search for relevant cached code curl -s -X POST https://api.raysurfer.com/api/retrieve/search \ -H "Authorization: Bearer $RAYSURFER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"task": "Send a Slack notification", "top_k": 3}' | jq . # 2. After your agent runs, upload the code it wrote curl -s -X POST https://api.raysurfer.com/api/store/execution-result \ -H "Authorization: Bearer $RAYSURFER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "task": "Send a Slack notification", "files_written": [ {"path": "notify.py", "content": "import requests\n\ndef send_slack(msg):\n requests.post(WEBHOOK, json={\"text\": msg})"} ], "succeeded": true }' | jq . ``` # Python SDK Source: https://docs.raysurfer.com/sdk/python Drop-in replacement for Claude Agent SDK ## Installation ```bash theme={null} uv add raysurfer ``` ## Setup Set your API key: ```bash theme={null} export RAYSURFER_API_KEY=your_api_key_here ``` Get your key from the [dashboard](https://www.raysurfer.com/dashboard/api-keys). ## Usage Swap your client class and method names. Options come directly from `claude_agent_sdk`: ```python theme={null} # Before from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions # After from raysurfer import RaysurferClient from claude_agent_sdk import ClaudeAgentOptions options = ClaudeAgentOptions( allowed_tools=["Read", "Write", "Bash"], system_prompt="You are a helpful assistant.", ) async with RaysurferClient(options) as client: await client.query("Generate quarterly report") async for msg in client.response(): print(msg) ``` ## Method Mapping | Claude SDK | Raysurfer | | ---------------------------- | ---------------------------- | | `ClaudeSDKClient(options)` | `RaysurferClient(options)` | | `await client.query(prompt)` | `await client.query(prompt)` | | `client.receive_response()` | `client.response()` | ## Options Options are passed through directly from `claude_agent_sdk.ClaudeAgentOptions`. All standard options work: ```python theme={null} from claude_agent_sdk import ClaudeAgentOptions options = ClaudeAgentOptions( allowed_tools=["Read", "Write", "Bash"], system_prompt="You are a helpful assistant.", model="claude-opus-4-6", max_turns=10, cwd="/path/to/working/dir", # ... all other ClaudeAgentOptions fields ) ``` Caching is enabled automatically when `RAYSURFER_API_KEY` is set. ## Agent-Owned Repos Set `agent_id` to isolate uploads/searches into an agent-owned repo: ```python theme={null} from raysurfer import AsyncRaySurfer client = AsyncRaySurfer( api_key="your_api_key", agent_id="claims-agent-v1", ) ``` ## Agent-Accessible Functions Decorator flow: ```python theme={null} from raysurfer import AsyncRaySurfer, agent_accessible rs = AsyncRaySurfer(api_key="your_api_key", agent_id="claims-agent-v1") @agent_accessible("Generate claim summary PDF") def generate_claim_summary(claim_id: str) -> str: return f"summary-{claim_id}.pdf" await rs.publish_function_registry([generate_claim_summary]) ``` `raysurfer.yaml` flow: ```python theme={null} from my_app import claims from raysurfer import AsyncRaySurfer, load_config rs = AsyncRaySurfer(api_key="your_api_key", agent_id="claims-agent-v1") functions = load_config("raysurfer.yaml", modules=[claims]) await rs.publish_function_registry(functions) ``` ## Snippet Retrieval Scope Control which cached snippets are retrieved using `snips_desired`: ```python theme={null} from raysurfer import RaysurferClient from claude_agent_sdk import ClaudeAgentOptions options = ClaudeAgentOptions( allowed_tools=["Read", "Write", "Bash"], ) # Include company-level snippets client = RaysurferClient( options, snips_desired="company", # Company-level snippets (Team/Enterprise) ) # Pro/Enterprise: Retrieve client-specific snippets client = RaysurferClient( options, snips_desired="client", # Client workspace snippets (Pro or Enterprise) ) ``` | Configuration | Required Tier | | ------------------------- | ------------------------------------------- | | `snips_desired="company"` | PRO or ENTERPRISE | | `snips_desired="client"` | PRO or ENTERPRISE (requires `workspace_id`) | ## Public Snippets Include community public snippets (crawled from GitHub) alongside your private snippets: ```python theme={null} # High-level client = RaysurferClient(options, public_snips=True) # Low-level client = RaySurfer(api_key="...", public_snips=True) ``` See [How It Works — Public Snippets](/how-it-works#public-snippets) for details. ## Full Example ```python theme={null} import asyncio import os from raysurfer import RaysurferClient from claude_agent_sdk import ClaudeAgentOptions os.environ["RAYSURFER_API_KEY"] = "your_api_key" async def main(): options = ClaudeAgentOptions( allowed_tools=["Read", "Write", "Bash"], system_prompt="You are a helpful assistant.", ) async with RaysurferClient(options) as client: # First run: generates and caches code await client.query("Fetch GitHub trending repos") async for msg in client.response(): print(msg) # Second run: retrieves from cache (instant) await client.query("Fetch GitHub trending repos") async for msg in client.response(): print(msg) asyncio.run(main()) ``` ## Without Caching If `RAYSURFER_API_KEY` is not set, `RaysurferClient` behaves exactly like `ClaudeSDKClient` — no caching, just a pass-through wrapper. ## Low-Level API For custom integrations, use the `RaySurfer` client directly. ### Complete Low-Level Example with Anthropic API ```python theme={null} import anthropic from raysurfer import RaySurfer from raysurfer.types import FileWritten, LogFile client = RaySurfer(api_key="your_raysurfer_api_key", agent_id="claims-agent-v1") task = "Fetch GitHub trending repos" # 1. Search for cached code matching a task result = client.search( task=task, top_k=5, min_verdict_score=0.3, per_function_reputation=True, ) for match in result.matches: print(f"{match.code_block.name}: {match.combined_score}") print(f" Source: {match.code_block.source[:80]}...") # Your LLM execution loop goes here # e.g. Claude Agent SDK generates and runs code using the cached snippets # 2. Upload a new code file after execution # AI voting is triggered automatically when use_raysurfer_ai_voting is True (default) file = FileWritten(path="fetch_repos.py", content="def fetch(): ...") client.upload( task=task, file_written=file, succeeded=True, execution_logs="Fetched 10 trending repos successfully", dependencies={"httpx": "0.27.0", "pydantic": "2.5.0"}, # Track package versions per_function_reputation=True, ) # 2b. Bulk upload prompts/logs/code for sandboxed grading logs = [LogFile(path="logs/run.log", content="Task completed", encoding="utf-8")] client.upload_bulk_code_snips( prompts=["Build a CLI tool", "Add CSV support"], files_written=[FileWritten(path="cli.py", content="def main(): ...")], log_files=logs, ) # 3. (Optional) Vote on a cached snippet manually # Only needed if you want explicit control — upload already votes by default client.vote_code_snip( task=task, code_block_id=result.matches[0].code_block.id, code_block_name=result.matches[0].code_block.name, code_block_description=result.matches[0].code_block.description, succeeded=True, ) ``` ### Response Fields The `search()` response includes: | Field | Type | Description | | ------------------- | ---------------- | -------------------------------------------------------- | | `files` | `list[CodeFile]` | Retrieved code files with metadata | | `task` | `str` | The task that was searched | | `total_found` | `int` | Total matches found | | `add_to_llm_prompt` | `str` | Pre-formatted string to append to your LLM system prompt | Each `CodeFile` contains: | Field | Type | Description | | ------------------ | ---------------- | --------------------------------------------------- | | `code_block_id` | `str` | Unique identifier of the code block | | `filename` | `str` | Generated filename (e.g., `"github_fetcher.py"`) | | `source` | `str` | Full source code | | `entrypoint` | `str` | Main function to call | | `description` | `str` | What the code does | | `input_schema` | `JsonDict` | JSON schema for inputs | | `output_schema` | `JsonDict` | JSON schema for outputs | | `language` | `str` | Programming language | | `dependencies` | `dict[str, str]` | Package name to version mapping | | `verdict_score` | `float` | Quality rating (thumbs\_up / total, 0.3 if unrated) | | `thumbs_up` | `int` | Positive votes | | `thumbs_down` | `int` | Negative votes | | `similarity_score` | `float` | Semantic similarity (0–1) | | `combined_score` | `float` | Combined score (similarity \* 0.6 + verdict \* 0.4) | ### Async Version ```python theme={null} import anthropic from raysurfer import AsyncRaySurfer from raysurfer.types import FileWritten async with AsyncRaySurfer(api_key="your_api_key") as client: # 1. Search for cached code result = await client.search( task="Fetch GitHub trending repos", per_function_reputation=True, ) for match in result.matches: print(f"{match.code_block.name}: {match.combined_score}") # Your LLM execution loop goes here # e.g. Claude Agent SDK generates and runs code using the cached snippets # 2. Upload a new code file after execution (voting triggered by default) file = FileWritten(path="fetch_repos.py", content="def fetch(): ...") await client.upload( task=task, file_written=file, succeeded=True, execution_logs="Fetched 10 trending repos successfully", per_function_reputation=True, ) # 3. (Optional) Vote on snippet manually — only if you need explicit control await client.vote_code_snip( task=task, code_block_id=result.matches[0].code_block.id, code_block_name=result.matches[0].code_block.name, code_block_description=result.matches[0].code_block.description, succeeded=True, ) ``` ### Search Response The `search()` method returns a `SearchResponse`: | Field | Type | Description | | ------------------- | ------------------- | -------------------------------------- | | `matches` | `list[SearchMatch]` | Matching code blocks with full scoring | | `total_found` | `int` | Total matches found | | `cache_hit` | `bool` | Whether results were served from cache | | `search_namespaces` | `list[str]` | Namespaces that were searched | Each `SearchMatch` contains: | Field | Type | Description | | ------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `code_block` | `CodeBlock` | The code block (has `id`, `name`, `description`, `source`, `entrypoint`, `language`, `dependencies`, `tags`) | | `combined_score` | `float` | Overall relevance score (0–1) | | `vector_score` | `float` | Semantic similarity score | | `verdict_score` | `float` | Community quality rating | | `error_resilience` | `float` | Error handling robustness score | | `thumbs_up` | `int` | Positive votes | | `thumbs_down` | `int` | Negative votes | | `filename` | `str` | Generated filename for the code | | `language` | `str` | Programming language | | `entrypoint` | `str` | Main function name | | `dependencies` | `dict[str, str]` | Package name to version mapping (e.g., `{"pandas": "2.1.0"}`) | | `agent_id` | `str \| None` | Agent identifier when snippet came from an agent-owned repo | | `s3_url` | `str \| None` | Presigned download URL for agent-stored script content | | `functions` | `list[FunctionReputation] \| None` | Per-function reputation metadata (when `per_function_reputation=True`) | ### Method Reference | Method | Description | | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | `search(task, top_k, min_verdict_score, prefer_complete, per_function_reputation)` | Search for cached code snippets | | `upload(task, file_written, succeeded, ..., per_function_reputation=False)` | Store a code file for future reuse | | `upload_bulk_code_snips(prompts, files_written, ...)` | Bulk upload for grading | | `delete(snippet_id)` | Delete a snippet by ID or name | | `vote_code_snip(task, code_block_id, name, description, succeeded)` | Vote on snippet usefulness | | `comment_on_code_snip(code_block_id, text)` | Add a comment to a snippet | | `publish_function_registry(functions)` | Publish `@agent_accessible` functions for agent discovery | | `tool(fn)` | Register a local function as a tool for `execute()` | | `execute(task, user_code=None, timeout=300, codegen_api_key=None, codegen_prompt=None, codegen_model=...)` | Execute user code (primary) or sandbox-generated code (optional) with registered tools | ## Programmatic Tool Calling Also available as an integration guide: [Register Python Functions](/integrations/register-python-functions). Register local tools, then either pass `user_code` (primary mode) or use optional sandbox codegen with your own key + prompt: ```python theme={null} import asyncio from raysurfer import AsyncRaySurfer async def main(): rs = AsyncRaySurfer(api_key="your_api_key") @rs.tool def add(a: int, b: int) -> int: """Add two numbers together.""" return a + b @rs.tool def multiply(a: int, b: int) -> int: """Multiply two numbers together.""" return a * b user_code = """ intermediate = add(5, 3) final = multiply(intermediate, 2) print(final) """ result = await rs.execute( "Add 5 and 3, then multiply the result by 2", user_code=user_code, ) print(result.result) # "16" print(result.tool_calls) # [ToolCallRecord(...), ...] print(result.cache_hit) # False (reserved field) asyncio.run(main()) ``` The `@rs.tool` decorator can wrap any Python function you want to expose as a tool. It introspects the function signature to build a JSON schema. Your function docstring becomes the tool `description` field in the tool schema payload. Both sync and async callbacks are supported. ### How It Works 1. SDK opens a live callback channel for tool call routing 2. Your app sends either `user_code` (primary mode) or `codegen_*` inputs (optional mode) to `/api/execute/run` 3. Code runs in a Modal sandbox — tool calls are routed back to your local functions through that callback channel 4. Results are returned with full tool call history Use exactly one mode per call: * `user_code`: run pre-generated code directly (recommended default) * `codegen_api_key` + `codegen_prompt`: generate code in sandbox and run it ### Execute Options ```python theme={null} result = await rs.execute( "Your task description", user_code="print(add(1, 2))", # Primary mode timeout=300, # Max execution time in seconds (default 300) ) result = await rs.execute( "Your task description", codegen_api_key="YOUR_ANTHROPIC_API_KEY", # Optional mode codegen_prompt="Write Python code that uses add(a, b) and prints 2 + 3.", codegen_model="claude-opus-4-6", timeout=300, ) ``` ### ExecuteResult Fields | Field | Type | Description | | -------------- | ---------------------- | ----------------------------------------------------- | | `execution_id` | `str` | Unique execution identifier | | `result` | `str \| None` | Stdout output from the script | | `exit_code` | `int` | Process exit code (0 = success) | | `duration_ms` | `int` | Total execution time | | `cache_hit` | `bool` | Reserved field (currently always `False` for execute) | | `error` | `str \| None` | Error message if exit\_code != 0 | | `tool_calls` | `list[ToolCallRecord]` | All tool calls made during execution | # TypeScript SDK Source: https://docs.raysurfer.com/sdk/typescript Drop-in replacement for Claude Agent SDK with automatic caching ## Installation ```bash theme={null} bun add raysurfer ``` ## Setup Set your API key: ```bash theme={null} export RAYSURFER_API_KEY=your_api_key_here ``` Get your key from the [dashboard](https://www.raysurfer.com/dashboard/api-keys). ## Usage Swap your import — everything else stays the same: ```typescript theme={null} // Before import { query } from "@anthropic-ai/claude-agent-sdk"; // After import { query } from "raysurfer"; for await (const message of query({ prompt: "Fetch data from GitHub API", options: { model: "claude-opus-4-6", systemPrompt: "You are a helpful assistant.", }, })) { console.log(message); } ``` All Claude SDK types are re-exported from `raysurfer`, so you don't need a separate import: ```typescript theme={null} import { query, type Options, type SDKMessage, type Query, } from "raysurfer"; ``` ## How It Works 1. **On query**: Retrieves cached code blocks matching your task 2. **Injects into prompt**: Agent sees proven code snippets 3. **After success**: New code is cached for next time Caching is enabled automatically when `RAYSURFER_API_KEY` is set. Without it, behaves exactly like the original SDK. ## Agent-Owned Repos Set `agentId` to isolate uploads/searches into an agent-owned repo: ```typescript theme={null} import { RaySurfer } from "raysurfer"; const client = new RaySurfer({ apiKey: "your_api_key", agentId: "claims-agent-v1", }); ``` ## Agent-Accessible Functions Decorator flow: ```typescript theme={null} import { RaySurfer, agentAccessible } from "raysurfer"; const rs = new RaySurfer({ apiKey: "your_api_key", agentId: "claims-agent-v1" }); const generateClaimSummary = agentAccessible( (claimId: string) => `summary-${claimId}.pdf`, { name: "generateClaimSummary", description: "Generate claim summary PDF", parameters: { claimId: "string" }, }, ); await rs.publishFunctionRegistry([generateClaimSummary]); ``` `raysurfer.yaml` flow: ```typescript theme={null} import * as claimsModule from "./src/claims"; import { RaySurfer, loadConfig } from "raysurfer"; const rs = new RaySurfer({ apiKey: "your_api_key", agentId: "claims-agent-v1" }); const functions = loadConfig("raysurfer.yaml", { "src/claims.ts": claimsModule, }); await rs.publishFunctionRegistry(functions); ``` ## Class-based API For users who prefer a class-based interface: ```typescript theme={null} import { ClaudeSDKClient } from "raysurfer"; const client = new ClaudeSDKClient({ model: "claude-opus-4-6", systemPrompt: "You are a helpful assistant.", }); for await (const msg of client.query("Fetch data from GitHub API")) { console.log(msg); } ``` `RaysurferClient` is also exported as an alias for `ClaudeSDKClient` — use whichever name you prefer. ## System Prompt Preset Use the Claude Code preset system prompt with appended instructions: ```typescript theme={null} for await (const message of query({ prompt: "Refactor the auth module", options: { systemPrompt: { type: "preset", preset: "claude_code", append: "Always explain your reasoning.", }, }, })) { console.log(message); } ``` ## Query Control Methods The `query()` function returns a `Query` object with full control methods: ```typescript theme={null} const q = query({ prompt: "Build a REST API" }); await q.interrupt(); await q.setPermissionMode("acceptEdits"); await q.setModel("claude-sonnet-4-5-20250929"); await q.setMaxThinkingTokens(4096); const models = await q.supportedModels(); const info = await q.accountInfo(); q.close(); ``` ## Snippet Retrieval Scope Control which cached snippets are retrieved: ```typescript theme={null} import { ClaudeSDKClient } from "raysurfer"; // Include company-level snippets (Team/Enterprise) const client = new ClaudeSDKClient({ snipsDesired: "company", }); // Pro/Enterprise: client-specific snippets const proClient = new ClaudeSDKClient({ snipsDesired: "client", }); ``` | Configuration | Required Tier | | ------------------------- | ------------------------------------------ | | Default (public only) | FREE | | `snipsDesired: "company"` | PRO or ENTERPRISE | | `snipsDesired: "client"` | PRO or ENTERPRISE (requires `workspaceId`) | ## Public Snippets Include community public snippets (crawled from GitHub) alongside your private snippets: ```typescript theme={null} // High-level const client = new ClaudeSDKClient({ publicSnips: true }); // Low-level const rs = new RaySurfer({ apiKey: "...", publicSnips: true }); ``` See [How It Works — Public Snippets](/how-it-works#public-snippets) for details. ## Low-Level API For custom integrations, use the `RaySurfer` client directly: ```typescript theme={null} import { RaySurfer } from "raysurfer"; const client = new RaySurfer({ apiKey: "your_api_key" }); // 1. Search for cached code const searchResult = await client.search({ task: "Fetch GitHub trending repos", perFunctionReputation: true, }); for (const match of searchResult.matches) { console.log(`${match.codeBlock.name}: ${match.combinedScore}`); } // Your LLM execution loop goes here // e.g. Claude Agent SDK generates and runs code using the cached snippets // 2. Upload a new code file after execution (with dependency versions) await client.upload({ task: "Fetch GitHub trending repos", fileWritten: { path: "fetch_repos.ts", content: "function fetch() { ... }" }, succeeded: true, dependencies: { "node-fetch": "3.3.0", "zod": "3.22.0" }, perFunctionReputation: true, }); // 2b. Bulk upload prompts/logs/code for sandboxed grading await client.uploadBulkCodeSnips( ["Build a CLI tool", "Add CSV support"], [{ path: "cli.ts", content: "function main() { ... }" }], [{ path: "logs/run.log", content: "Task completed", encoding: "utf-8" }], true ); // 3. Vote on whether a cached snippet was useful await client.voteCodeSnip({ task: "Fetch GitHub trending repos", codeBlockId: "abc123", codeBlockName: "github_fetcher", codeBlockDescription: "Fetches trending repos from GitHub", succeeded: true, }); ``` ### Low-Level Client Options ```typescript theme={null} const client = new RaySurfer({ apiKey: "your_api_key", baseUrl: "https://api.raysurfer.com", // optional timeout: 30000, // optional, in ms organizationId: "org_xxx", // optional, for team namespacing workspaceId: "ws_xxx", // optional, for per-customer namespacing (Pro or Enterprise) agentId: "claims-agent-v1", // optional, for agent-owned repo isolation snipsDesired: "company", // optional, snippet scope publicSnips: true, // optional, include community snippets }); ``` ### Response Fields The `search()` response includes: | Field | Type | Description | | ---------------- | ------------ | --------------------------------------------------- | | `files` | `CodeFile[]` | Retrieved code files with metadata | | `task` | `string` | The task that was searched | | `totalFound` | `number` | Total matches found | | `addToLlmPrompt` | `string` | Pre-formatted string to append to LLM system prompt | Each `CodeFile` contains: | Field | Type | Description | | ----------------- | --------------------------- | --------------------------------------------------- | | `codeBlockId` | `string` | Unique identifier of the code block | | `filename` | `string` | Generated filename (e.g., `"github_fetcher.ts"`) | | `source` | `string` | Full source code | | `entrypoint` | `string` | Main function to call | | `description` | `string` | What the code does | | `inputSchema` | `Record` | JSON schema for inputs | | `outputSchema` | `Record` | JSON schema for outputs | | `language` | `string` | Programming language | | `dependencies` | `Record` | Package name to version mapping | | `verdictScore` | `number` | Quality rating (thumbsUp / total, 0.3 if unrated) | | `thumbsUp` | `number` | Positive votes | | `thumbsDown` | `number` | Negative votes | | `similarityScore` | `number` | Semantic similarity (0–1) | | `combinedScore` | `number` | Combined score (similarity \* 0.6 + verdict \* 0.4) | ### Search Response The `search()` method returns a `SearchResponse`: | Field | Type | Description | | ------------------ | --------------- | -------------------------------------- | | `matches` | `SearchMatch[]` | Matching code blocks with full scoring | | `totalFound` | `number` | Total matches found | | `cacheHit` | `boolean` | Whether results were served from cache | | `searchNamespaces` | `string[]` | Namespaces that were searched | Each `SearchMatch` contains: | Field | Type | Description | | ----------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------ | | `codeBlock` | `CodeBlock` | The code block (has `id`, `name`, `description`, `source`, `entrypoint`, `language`, `dependencies`, `tags`) | | `combinedScore` | `number` | Overall relevance score (0–1) | | `vectorScore` | `number` | Semantic similarity score | | `verdictScore` | `number` | Community quality rating | | `errorResilience` | `number` | Error handling robustness score | | `thumbsUp` | `number` | Positive votes | | `thumbsDown` | `number` | Negative votes | | `filename` | `string` | Generated filename for the code | | `language` | `string` | Programming language | | `entrypoint` | `string` | Main function name | | `dependencies` | `Record` | Package name to version mapping (e.g., `{"node-fetch": "3.3.0"}`) | | `agentId` | `string \| null` | Agent identifier when snippet came from an agent-owned repo | | `s3Url` | `string \| null` | Presigned download URL for agent-stored script content | | `functions` | `FunctionReputation[] \| null` | Per-function reputation metadata (when `perFunctionReputation: true`) | ### Method Reference | Method | Description | | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | `search({ task, topK?, minVerdictScore?, preferComplete?, perFunctionReputation? })` | Search for cached code snippets | | `upload({ task, fileWritten, succeeded, perFunctionReputation?, ... })` | Store a code file for future reuse | | `uploadBulkCodeSnips({ prompts, filesWritten, ... })` | Bulk upload for grading | | `delete(snippetId)` | Delete a snippet by ID or name | | `voteCodeSnip({ task, codeBlockId, codeBlockName, codeBlockDescription, succeeded })` | Vote on snippet usefulness | | `commentOnCodeSnip({ codeBlockId, text })` | Add a comment to a snippet | | `publishFunctionRegistry(functions)` | Publish `agentAccessible(...)` functions for agent discovery | | `tool(name, description, parameters, callback)` | Register a local function as a tool for `execute()` | | `execute(task, { userCode?, codegen?, timeout? })` | Execute user code (primary) or sandbox-generated code (optional) with registered tools | ## Programmatic Tool Calling Also available as an integration guide: [Register TypeScript Functions](/integrations/register-ts-functions). Register local tools, then either pass `userCode` (primary mode) or use optional sandbox codegen with your own key + prompt: ```typescript theme={null} import { RaySurfer } from "raysurfer"; const rs = new RaySurfer({ apiKey: "your_api_key" }); rs.tool("add", "Add two numbers", { a: "integer", b: "integer" }, async (args) => String(Number(args.a) + Number(args.b)) ); rs.tool("multiply", "Multiply two numbers", { a: "integer", b: "integer" }, async (args) => String(Number(args.a) * Number(args.b)) ); const userCode = ` intermediate = add(5, 3) final = multiply(intermediate, 2) print(final) `; const result = await rs.execute("Add 5 and 3, then multiply the result by 2", { userCode, }); console.log(result.result); // "16" console.log(result.toolCalls); // [{toolName: 'add', ...}, ...] console.log(result.cacheHit); // false (reserved field) ``` `tool()` lets you wrap any local function as a tool callback. The `description` string you pass is included in the tool schema payload. ### How It Works 1. SDK opens a live callback channel for tool call routing 2. Your app sends either `userCode` (primary mode) or `codegen` (optional mode) to `/api/execute/run` 3. Code runs in a Modal sandbox — tool calls are routed back to your local functions through that callback channel 4. Results are returned with full tool call history Use exactly one mode per call: * `userCode`: run pre-generated code directly (recommended default) * `codegen`: generate code in sandbox and run it ### Execute Options ```typescript theme={null} const result = await rs.execute("Your task description", { userCode: "print(add(1, 2))", // Primary mode timeout: 300000, // Max time in ms (default 300000) }); const sandboxCodegenResult = await rs.execute("Your task description", { codegen: { provider: "anthropic", apiKey: "YOUR_ANTHROPIC_API_KEY", model: "claude-opus-4-6", prompt: "Write Python code that uses add(a, b) and prints 2 + 3.", }, timeout: 300000, }); ``` ### ExecuteResult Fields | Field | Type | Description | | ------------- | ------------------ | ----------------------------------------------------- | | `executionId` | `string` | Unique execution identifier | | `result` | `string \| null` | Stdout output from the script | | `exitCode` | `number` | Process exit code (0 = success) | | `durationMs` | `number` | Total execution time | | `cacheHit` | `boolean` | Reserved field (currently always `false` for execute) | | `error` | `string \| null` | Error message if exitCode != 0 | | `toolCalls` | `ToolCallRecord[]` | All tool calls made during execution |