raysurfer 0.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- raysurfer/__init__.py +101 -0
- raysurfer/client.py +773 -0
- raysurfer/exceptions.py +21 -0
- raysurfer/sdk_client.py +552 -0
- raysurfer/sdk_types.py +30 -0
- raysurfer/types.py +198 -0
- raysurfer-0.4.1.dist-info/METADATA +157 -0
- raysurfer-0.4.1.dist-info/RECORD +9 -0
- raysurfer-0.4.1.dist-info/WHEEL +4 -0
raysurfer/exceptions.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""RaySurfer SDK exceptions"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RaySurferError(Exception):
|
|
5
|
+
"""Base exception for RaySurfer SDK"""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class APIError(RaySurferError):
|
|
11
|
+
"""API returned an error response"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, message: str, status_code: int | None = None):
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
self.status_code = status_code
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AuthenticationError(RaySurferError):
|
|
19
|
+
"""Authentication failed"""
|
|
20
|
+
|
|
21
|
+
pass
|
raysurfer/sdk_client.py
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Drop-in replacement for Claude Agent SDK with automatic code caching.
|
|
3
|
+
|
|
4
|
+
Simply swap your import and rename your client:
|
|
5
|
+
# Before
|
|
6
|
+
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
|
|
7
|
+
client = ClaudeSDKClient(options)
|
|
8
|
+
await client.query("task")
|
|
9
|
+
|
|
10
|
+
# After
|
|
11
|
+
from raysurfer import RaysurferClient
|
|
12
|
+
from claude_agent_sdk import ClaudeAgentOptions
|
|
13
|
+
client = RaysurferClient(options)
|
|
14
|
+
await client.raysurfer_query("task")
|
|
15
|
+
|
|
16
|
+
Options come directly from claude_agent_sdk - no Raysurfer-specific options needed.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import atexit
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import shutil
|
|
24
|
+
import tempfile
|
|
25
|
+
from typing import Any, AsyncIterator
|
|
26
|
+
|
|
27
|
+
# Isolate temp directory to avoid file watcher conflicts when running
|
|
28
|
+
# nested inside another Claude Code session.
|
|
29
|
+
_ISOLATED_TMPDIR: str | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _setup_isolated_env() -> str:
|
|
33
|
+
"""Set up isolated temp directory for nested Claude Code execution."""
|
|
34
|
+
global _ISOLATED_TMPDIR
|
|
35
|
+
|
|
36
|
+
if _ISOLATED_TMPDIR is None:
|
|
37
|
+
_ISOLATED_TMPDIR = tempfile.mkdtemp(prefix="raysurfer_sdk_")
|
|
38
|
+
os.environ["TMPDIR"] = _ISOLATED_TMPDIR
|
|
39
|
+
os.environ["TEMP"] = _ISOLATED_TMPDIR
|
|
40
|
+
os.environ["TMP"] = _ISOLATED_TMPDIR
|
|
41
|
+
os.environ["CLAUDE_AGENT_SDK_SKIP_VERSION_CHECK"] = "1"
|
|
42
|
+
atexit.register(_cleanup_isolated_env)
|
|
43
|
+
|
|
44
|
+
return _ISOLATED_TMPDIR
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _cleanup_isolated_env() -> None:
|
|
48
|
+
"""Clean up isolated temp directory on exit."""
|
|
49
|
+
global _ISOLATED_TMPDIR
|
|
50
|
+
if _ISOLATED_TMPDIR and os.path.exists(_ISOLATED_TMPDIR):
|
|
51
|
+
try:
|
|
52
|
+
shutil.rmtree(_ISOLATED_TMPDIR)
|
|
53
|
+
except Exception:
|
|
54
|
+
pass
|
|
55
|
+
_ISOLATED_TMPDIR = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_setup_isolated_env()
|
|
59
|
+
|
|
60
|
+
# Import from Claude Agent SDK - these are passed through directly
|
|
61
|
+
from claude_agent_sdk import ClaudeAgentOptions
|
|
62
|
+
from claude_agent_sdk import ClaudeSDKClient as _BaseClaudeSDKClient
|
|
63
|
+
from claude_agent_sdk import (
|
|
64
|
+
AgentDefinition,
|
|
65
|
+
AssistantMessage,
|
|
66
|
+
HookMatcher,
|
|
67
|
+
Message,
|
|
68
|
+
ResultMessage,
|
|
69
|
+
SystemMessage,
|
|
70
|
+
TextBlock,
|
|
71
|
+
ThinkingBlock,
|
|
72
|
+
ToolResultBlock,
|
|
73
|
+
ToolUseBlock,
|
|
74
|
+
UserMessage,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
from raysurfer.client import AsyncRaySurfer
|
|
78
|
+
from raysurfer.types import FileWritten, SnipsDesired
|
|
79
|
+
|
|
80
|
+
logger = logging.getLogger(__name__)
|
|
81
|
+
|
|
82
|
+
# Re-export all SDK types for convenience
|
|
83
|
+
__all__ = [
|
|
84
|
+
"RaysurferClient",
|
|
85
|
+
# Re-exported from Claude Agent SDK (use these directly)
|
|
86
|
+
"ClaudeAgentOptions",
|
|
87
|
+
"AgentDefinition",
|
|
88
|
+
"HookMatcher",
|
|
89
|
+
"Message",
|
|
90
|
+
"UserMessage",
|
|
91
|
+
"AssistantMessage",
|
|
92
|
+
"SystemMessage",
|
|
93
|
+
"ResultMessage",
|
|
94
|
+
"TextBlock",
|
|
95
|
+
"ThinkingBlock",
|
|
96
|
+
"ToolUseBlock",
|
|
97
|
+
"ToolResultBlock",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
# Default backend URL
|
|
101
|
+
DEFAULT_RAYSURFER_URL = "https://web-production-3d338.up.railway.app"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class RaysurferClient:
|
|
105
|
+
"""
|
|
106
|
+
Drop-in replacement for ClaudeSDKClient with automatic Raysurfer caching.
|
|
107
|
+
|
|
108
|
+
Usage:
|
|
109
|
+
from raysurfer import RaysurferClient
|
|
110
|
+
from claude_agent_sdk import ClaudeAgentOptions
|
|
111
|
+
|
|
112
|
+
options = ClaudeAgentOptions(
|
|
113
|
+
allowed_tools=["Read", "Write", "Bash"],
|
|
114
|
+
system_prompt="You are a helpful assistant.",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
async with RaysurferClient(options) as client:
|
|
118
|
+
await client.raysurfer_query("Fetch data from GitHub API")
|
|
119
|
+
async for msg in client.raysurfer_response():
|
|
120
|
+
print(msg)
|
|
121
|
+
|
|
122
|
+
Features:
|
|
123
|
+
- Automatic cache retrieval and system prompt augmentation
|
|
124
|
+
- Multi-agent support: subagent prompts are also augmented with cache
|
|
125
|
+
- Bash file tracking: detects files created by Bash commands
|
|
126
|
+
- Hook propagation: user hooks are preserved and work alongside cache hooks
|
|
127
|
+
|
|
128
|
+
Set RAYSURFER_API_KEY environment variable to enable caching.
|
|
129
|
+
Options come directly from claude_agent_sdk.ClaudeAgentOptions.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
# File extensions we track from Bash output
|
|
133
|
+
TRACKABLE_EXTENSIONS = {
|
|
134
|
+
".py", ".js", ".ts", ".rb", ".go", ".rs", ".java", ".cpp", ".c", ".h",
|
|
135
|
+
".pdf", ".docx", ".xlsx", ".csv", ".json", ".yaml", ".yml", ".xml",
|
|
136
|
+
".html", ".css", ".md", ".txt", ".sh", ".sql"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
def __init__(
|
|
140
|
+
self,
|
|
141
|
+
options: ClaudeAgentOptions | None = None,
|
|
142
|
+
public_snips: bool = False,
|
|
143
|
+
snips_desired: SnipsDesired | str | None = None,
|
|
144
|
+
):
|
|
145
|
+
"""
|
|
146
|
+
Initialize RaysurferClient.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
options: ClaudeAgentOptions from claude_agent_sdk (passed through directly)
|
|
150
|
+
public_snips: Whether to include public/shared snippets in retrieval (default: False)
|
|
151
|
+
snips_desired: Scope of private snippets - "company" (Team/Enterprise) or "client" (Enterprise only)
|
|
152
|
+
"""
|
|
153
|
+
self._options = options or ClaudeAgentOptions()
|
|
154
|
+
self._base_client: _BaseClaudeSDKClient | None = None
|
|
155
|
+
self._raysurfer: AsyncRaySurfer | None = None
|
|
156
|
+
self._current_query: str | None = None
|
|
157
|
+
self._generated_files: list[FileWritten] = []
|
|
158
|
+
self._bash_generated_files: list[str] = []
|
|
159
|
+
self._task_succeeded: bool = False
|
|
160
|
+
self._cache_enabled: bool = False
|
|
161
|
+
self._cached_code_blocks: list[dict[str, Any]] = []
|
|
162
|
+
self._subagent_cache: dict[str, str] = {}
|
|
163
|
+
self._public_snips = public_snips
|
|
164
|
+
self._snips_desired = snips_desired
|
|
165
|
+
|
|
166
|
+
async def __aenter__(self) -> "RaysurferClient":
|
|
167
|
+
"""Initialize the client."""
|
|
168
|
+
api_key = os.environ.get("RAYSURFER_API_KEY")
|
|
169
|
+
base_url = os.environ.get("RAYSURFER_BASE_URL", DEFAULT_RAYSURFER_URL)
|
|
170
|
+
|
|
171
|
+
if api_key:
|
|
172
|
+
self._cache_enabled = True
|
|
173
|
+
self._raysurfer = AsyncRaySurfer(
|
|
174
|
+
api_key=api_key,
|
|
175
|
+
base_url=base_url,
|
|
176
|
+
public_snips=self._public_snips,
|
|
177
|
+
snips_desired=self._snips_desired,
|
|
178
|
+
)
|
|
179
|
+
await self._raysurfer.__aenter__()
|
|
180
|
+
|
|
181
|
+
return self
|
|
182
|
+
|
|
183
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
184
|
+
"""Clean up resources."""
|
|
185
|
+
if self._base_client:
|
|
186
|
+
await self._base_client.__aexit__(exc_type, exc_val, exc_tb)
|
|
187
|
+
if self._raysurfer:
|
|
188
|
+
await self._raysurfer.__aexit__(exc_type, exc_val, exc_tb)
|
|
189
|
+
|
|
190
|
+
async def raysurfer_query(self, prompt: str) -> None:
|
|
191
|
+
"""
|
|
192
|
+
Send a query to Claude with Raysurfer caching.
|
|
193
|
+
|
|
194
|
+
If RAYSURFER_API_KEY is set, automatically retrieves relevant cached
|
|
195
|
+
code and injects it into the system prompt.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
prompt: The task/query to send to Claude
|
|
199
|
+
"""
|
|
200
|
+
self._current_query = prompt
|
|
201
|
+
self._generated_files = []
|
|
202
|
+
self._bash_generated_files = []
|
|
203
|
+
self._task_succeeded = False
|
|
204
|
+
self._cached_code_blocks = []
|
|
205
|
+
self._subagent_cache = {}
|
|
206
|
+
|
|
207
|
+
# Pre-fetch cache for subagents if this is a multi-agent system
|
|
208
|
+
if self._options.agents:
|
|
209
|
+
await self._fetch_subagent_cache(self._options.agents)
|
|
210
|
+
|
|
211
|
+
# Retrieve cached code if caching is enabled
|
|
212
|
+
augmented_options = await self._augment_options_with_cache(prompt)
|
|
213
|
+
|
|
214
|
+
# Initialize and query the base client
|
|
215
|
+
self._base_client = _BaseClaudeSDKClient(options=augmented_options)
|
|
216
|
+
await self._base_client.__aenter__()
|
|
217
|
+
await self._base_client.query(prompt)
|
|
218
|
+
|
|
219
|
+
async def raysurfer_response(self) -> AsyncIterator[Message]:
|
|
220
|
+
"""
|
|
221
|
+
Receive and yield response messages from Claude.
|
|
222
|
+
|
|
223
|
+
After successful task completion, automatically uploads any
|
|
224
|
+
generated code to the Raysurfer cache.
|
|
225
|
+
|
|
226
|
+
Yields:
|
|
227
|
+
Message objects from Claude
|
|
228
|
+
"""
|
|
229
|
+
if not self._base_client:
|
|
230
|
+
raise RuntimeError("Must call raysurfer_query() before raysurfer_response()")
|
|
231
|
+
|
|
232
|
+
last_bash_command: str | None = None
|
|
233
|
+
|
|
234
|
+
async for message in self._base_client.receive_response():
|
|
235
|
+
# Track Write and Bash tool calls
|
|
236
|
+
if isinstance(message, AssistantMessage):
|
|
237
|
+
for block in message.content:
|
|
238
|
+
if isinstance(block, ToolUseBlock):
|
|
239
|
+
if block.name == "Write":
|
|
240
|
+
self._track_file(block.input)
|
|
241
|
+
elif block.name == "Bash":
|
|
242
|
+
last_bash_command = block.input.get("command", "")
|
|
243
|
+
self._track_bash_file_outputs(last_bash_command)
|
|
244
|
+
|
|
245
|
+
# Track files from Bash tool results
|
|
246
|
+
if isinstance(message, AssistantMessage):
|
|
247
|
+
for block in message.content:
|
|
248
|
+
if isinstance(block, ToolResultBlock) and last_bash_command:
|
|
249
|
+
self._extract_files_from_bash_output(
|
|
250
|
+
last_bash_command,
|
|
251
|
+
str(block.content) if hasattr(block, 'content') else ""
|
|
252
|
+
)
|
|
253
|
+
last_bash_command = None
|
|
254
|
+
|
|
255
|
+
# Check for successful completion
|
|
256
|
+
if isinstance(message, ResultMessage):
|
|
257
|
+
if message.subtype == "success":
|
|
258
|
+
self._task_succeeded = True
|
|
259
|
+
|
|
260
|
+
yield message
|
|
261
|
+
|
|
262
|
+
# Upload generated code if task succeeded
|
|
263
|
+
if self._cache_enabled and self._task_succeeded:
|
|
264
|
+
if self._generated_files:
|
|
265
|
+
await self._upload_to_cache()
|
|
266
|
+
await self._cache_bash_generated_files()
|
|
267
|
+
|
|
268
|
+
# Submit votes for cached code blocks that were used
|
|
269
|
+
if self._cache_enabled and self._task_succeeded and self._cached_code_blocks:
|
|
270
|
+
await self._submit_votes()
|
|
271
|
+
|
|
272
|
+
def _track_file(self, tool_input: dict[str, Any]) -> None:
|
|
273
|
+
"""Track a file written by the Write tool."""
|
|
274
|
+
file_path = tool_input.get("file_path", "")
|
|
275
|
+
content = tool_input.get("content", "")
|
|
276
|
+
if file_path and content:
|
|
277
|
+
self._generated_files.append(FileWritten(path=file_path, content=content))
|
|
278
|
+
|
|
279
|
+
def _track_bash_file_outputs(self, command: str) -> None:
|
|
280
|
+
"""Extract potential output files from Bash commands."""
|
|
281
|
+
patterns = [
|
|
282
|
+
r'>\s*([^\s;&|]+)',
|
|
283
|
+
r'>>\s*([^\s;&|]+)',
|
|
284
|
+
r'-o\s+([^\s;&|]+)',
|
|
285
|
+
r'--output[=\s]+([^\s;&|]+)',
|
|
286
|
+
r'savefig\([\'"]([^\'"]+)[\'"]\)',
|
|
287
|
+
r'to_csv\([\'"]([^\'"]+)[\'"]\)',
|
|
288
|
+
r'to_excel\([\'"]([^\'"]+)[\'"]\)',
|
|
289
|
+
r'write\([\'"]([^\'"]+)[\'"]\)',
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
for pattern in patterns:
|
|
293
|
+
matches = re.findall(pattern, command)
|
|
294
|
+
for match in matches:
|
|
295
|
+
ext = os.path.splitext(match)[1].lower()
|
|
296
|
+
if ext in self.TRACKABLE_EXTENSIONS:
|
|
297
|
+
self._bash_generated_files.append(match)
|
|
298
|
+
|
|
299
|
+
def _extract_files_from_bash_output(self, command: str, output: str) -> None:
|
|
300
|
+
"""Extract files mentioned in Bash command output."""
|
|
301
|
+
file_pattern = r'[/\w.-]+\.\w{2,5}'
|
|
302
|
+
matches = re.findall(file_pattern, output)
|
|
303
|
+
for match in matches:
|
|
304
|
+
ext = os.path.splitext(match)[1].lower()
|
|
305
|
+
if ext in self.TRACKABLE_EXTENSIONS:
|
|
306
|
+
if match not in self._bash_generated_files:
|
|
307
|
+
self._bash_generated_files.append(match)
|
|
308
|
+
|
|
309
|
+
async def _cache_bash_generated_files(self) -> None:
|
|
310
|
+
"""Attempt to read and cache files generated by Bash commands."""
|
|
311
|
+
if not self._raysurfer or not self._bash_generated_files:
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
for file_path in self._bash_generated_files:
|
|
315
|
+
try:
|
|
316
|
+
ext = os.path.splitext(file_path)[1].lower()
|
|
317
|
+
if ext in {".py", ".js", ".ts", ".rb", ".go", ".rs", ".java",
|
|
318
|
+
".json", ".yaml", ".yml", ".xml", ".html", ".css",
|
|
319
|
+
".md", ".txt", ".sh", ".sql", ".csv"}:
|
|
320
|
+
if os.path.exists(file_path) and os.path.getsize(file_path) < 100000:
|
|
321
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
322
|
+
content = f.read()
|
|
323
|
+
if content.strip():
|
|
324
|
+
self._generated_files.append(FileWritten(
|
|
325
|
+
path=file_path,
|
|
326
|
+
content=content
|
|
327
|
+
))
|
|
328
|
+
logger.debug(f"Tracked Bash-generated file: {file_path}")
|
|
329
|
+
except Exception as e:
|
|
330
|
+
logger.debug(f"Could not read Bash-generated file {file_path}: {e}")
|
|
331
|
+
|
|
332
|
+
def _augment_subagent_prompts(self, agents: dict[str, AgentDefinition] | None) -> dict[str, AgentDefinition] | None:
|
|
333
|
+
"""Augment subagent prompts with cached code snippets."""
|
|
334
|
+
if not agents or not self._cache_enabled:
|
|
335
|
+
return agents
|
|
336
|
+
|
|
337
|
+
augmented_agents = {}
|
|
338
|
+
for name, agent_def in agents.items():
|
|
339
|
+
subagent_snippet = self._subagent_cache.get(name, "")
|
|
340
|
+
if subagent_snippet:
|
|
341
|
+
augmented_prompt = (agent_def.prompt or "") + subagent_snippet
|
|
342
|
+
augmented_agents[name] = AgentDefinition(
|
|
343
|
+
description=agent_def.description,
|
|
344
|
+
prompt=augmented_prompt,
|
|
345
|
+
tools=agent_def.tools,
|
|
346
|
+
model=agent_def.model,
|
|
347
|
+
)
|
|
348
|
+
else:
|
|
349
|
+
augmented_agents[name] = agent_def
|
|
350
|
+
|
|
351
|
+
return augmented_agents
|
|
352
|
+
|
|
353
|
+
async def _fetch_subagent_cache(self, agents: dict[str, AgentDefinition] | None) -> None:
|
|
354
|
+
"""Pre-fetch cache snippets for all subagents."""
|
|
355
|
+
if not agents or not self._cache_enabled or not self._raysurfer:
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
for name, agent_def in agents.items():
|
|
359
|
+
try:
|
|
360
|
+
task = agent_def.description or name
|
|
361
|
+
response = await self._raysurfer.get_code_files(
|
|
362
|
+
task=task,
|
|
363
|
+
top_k=3,
|
|
364
|
+
min_verdict_score=0.3,
|
|
365
|
+
prefer_complete=True,
|
|
366
|
+
)
|
|
367
|
+
if response.files:
|
|
368
|
+
self._subagent_cache[name] = self._format_code_snippets(response.files)
|
|
369
|
+
logger.debug(f"Cached {len(response.files)} code blocks for subagent: {name}")
|
|
370
|
+
except Exception as e:
|
|
371
|
+
logger.debug(f"Failed to fetch cache for subagent {name}: {e}")
|
|
372
|
+
|
|
373
|
+
async def _augment_options_with_cache(self, task: str) -> ClaudeAgentOptions:
|
|
374
|
+
"""Retrieve cached code, write to filesystem, and tell LLM where files are."""
|
|
375
|
+
if not self._cache_enabled or not self._raysurfer:
|
|
376
|
+
return self._options
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
response = await self._raysurfer.get_code_files(
|
|
380
|
+
task=task,
|
|
381
|
+
top_k=5,
|
|
382
|
+
min_verdict_score=0.3,
|
|
383
|
+
prefer_complete=True,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
if not response.files:
|
|
387
|
+
return self._options
|
|
388
|
+
|
|
389
|
+
self._cached_code_blocks = [
|
|
390
|
+
{
|
|
391
|
+
"code_block_id": f.code_block_id,
|
|
392
|
+
"filename": f.filename,
|
|
393
|
+
"description": f.description,
|
|
394
|
+
}
|
|
395
|
+
for f in response.files
|
|
396
|
+
]
|
|
397
|
+
|
|
398
|
+
written_files = self._write_cached_files_to_disk(response.files)
|
|
399
|
+
|
|
400
|
+
if not written_files:
|
|
401
|
+
return self._options
|
|
402
|
+
|
|
403
|
+
cache_notice = self._format_cache_file_notice(written_files)
|
|
404
|
+
base_prompt = self._options.system_prompt
|
|
405
|
+
|
|
406
|
+
if isinstance(base_prompt, dict) and base_prompt.get("type") == "preset":
|
|
407
|
+
augmented_prompt = {
|
|
408
|
+
**base_prompt,
|
|
409
|
+
"append": base_prompt.get("append", "") + cache_notice,
|
|
410
|
+
}
|
|
411
|
+
else:
|
|
412
|
+
augmented_prompt = (base_prompt or "") + cache_notice
|
|
413
|
+
|
|
414
|
+
augmented_agents = self._augment_subagent_prompts(self._options.agents)
|
|
415
|
+
|
|
416
|
+
# Create new options with augmented prompt - pass through all other options
|
|
417
|
+
return ClaudeAgentOptions(
|
|
418
|
+
allowed_tools=self._options.allowed_tools,
|
|
419
|
+
disallowed_tools=self._options.disallowed_tools,
|
|
420
|
+
permission_mode=self._options.permission_mode,
|
|
421
|
+
system_prompt=augmented_prompt,
|
|
422
|
+
cwd=self._options.cwd,
|
|
423
|
+
add_dirs=self._options.add_dirs,
|
|
424
|
+
max_turns=self._options.max_turns,
|
|
425
|
+
model=self._options.model,
|
|
426
|
+
env=self._options.env,
|
|
427
|
+
mcp_servers=self._options.mcp_servers,
|
|
428
|
+
hooks=self._options.hooks,
|
|
429
|
+
can_use_tool=self._options.can_use_tool,
|
|
430
|
+
setting_sources=self._options.setting_sources,
|
|
431
|
+
include_partial_messages=self._options.include_partial_messages,
|
|
432
|
+
fork_session=self._options.fork_session,
|
|
433
|
+
continue_conversation=self._options.continue_conversation,
|
|
434
|
+
resume=self._options.resume,
|
|
435
|
+
agents=augmented_agents,
|
|
436
|
+
plugins=self._options.plugins,
|
|
437
|
+
enable_file_checkpointing=self._options.enable_file_checkpointing,
|
|
438
|
+
output_format=self._options.output_format,
|
|
439
|
+
sandbox=self._options.sandbox,
|
|
440
|
+
extra_args=self._options.extra_args,
|
|
441
|
+
max_buffer_size=self._options.max_buffer_size,
|
|
442
|
+
stderr=self._options.stderr,
|
|
443
|
+
user=self._options.user,
|
|
444
|
+
settings=self._options.settings,
|
|
445
|
+
permission_prompt_tool_name=self._options.permission_prompt_tool_name,
|
|
446
|
+
)
|
|
447
|
+
except Exception as e:
|
|
448
|
+
logger.debug(f"Failed to retrieve cached code: {e}")
|
|
449
|
+
return self._options
|
|
450
|
+
|
|
451
|
+
def _write_cached_files_to_disk(self, files) -> list[dict[str, str]]:
|
|
452
|
+
"""Write cached code files to the working directory."""
|
|
453
|
+
written_files = []
|
|
454
|
+
cwd = self._options.cwd or os.getcwd()
|
|
455
|
+
|
|
456
|
+
cache_dir = os.path.join(cwd, "raysurfer_relevant_workflow_cache")
|
|
457
|
+
|
|
458
|
+
# Clear existing cache to avoid stale files from previous runs
|
|
459
|
+
if os.path.exists(cache_dir):
|
|
460
|
+
shutil.rmtree(cache_dir)
|
|
461
|
+
os.makedirs(cache_dir)
|
|
462
|
+
|
|
463
|
+
for f in files:
|
|
464
|
+
try:
|
|
465
|
+
file_path = os.path.join(cache_dir, f.filename)
|
|
466
|
+
os.makedirs(os.path.dirname(file_path), exist_ok=True) if os.path.dirname(file_path) != cache_dir else None
|
|
467
|
+
|
|
468
|
+
with open(file_path, 'w', encoding='utf-8') as out_file:
|
|
469
|
+
out_file.write(f.source)
|
|
470
|
+
|
|
471
|
+
written_files.append({
|
|
472
|
+
"path": file_path,
|
|
473
|
+
"filename": f.filename,
|
|
474
|
+
"description": f.description,
|
|
475
|
+
"entrypoint": f.entrypoint,
|
|
476
|
+
"language": f.language,
|
|
477
|
+
"confidence": f.verdict_score,
|
|
478
|
+
})
|
|
479
|
+
logger.debug(f"Wrote cached file: {file_path}")
|
|
480
|
+
except Exception as e:
|
|
481
|
+
logger.debug(f"Failed to write cached file {f.filename}: {e}")
|
|
482
|
+
|
|
483
|
+
return written_files
|
|
484
|
+
|
|
485
|
+
def _format_cache_file_notice(self, written_files: list[dict[str, str]]) -> str:
|
|
486
|
+
"""Format a minimal notice telling the LLM where cached files are."""
|
|
487
|
+
notice = "\n\n## IMPORTANT: Pre-validated Workflow Code Available - USE THESE FILES\n\n"
|
|
488
|
+
notice += "**DO NOT REWRITE CODE.** The following validated workflow code has been retrieved from the cloud cache "
|
|
489
|
+
notice += "and saved to `raysurfer_relevant_workflow_cache/`. Use these files directly:\n\n"
|
|
490
|
+
|
|
491
|
+
for f in written_files:
|
|
492
|
+
notice += f"### `{f['filename']}` → `{f['path']}`\n"
|
|
493
|
+
notice += f"- **What it does**: {f['description']}\n"
|
|
494
|
+
notice += f"- **Run with**: `python {f['path']}`\n"
|
|
495
|
+
notice += f"- **Copy to destination**: `cp {f['path']} ./`\n\n"
|
|
496
|
+
|
|
497
|
+
notice += "**Instructions**:\n"
|
|
498
|
+
notice += "1. If the task asks to create a file that matches one above, just COPY it: `cp <cached_path> <destination>`\n"
|
|
499
|
+
notice += "2. If you need to execute, run the cached file directly: `python <cached_path>`\n"
|
|
500
|
+
notice += "3. Only Read and modify if the task requires changes not covered by the cached file\n"
|
|
501
|
+
notice += "4. **DO NOT regenerate code that already exists** - this wastes time\n"
|
|
502
|
+
|
|
503
|
+
return notice
|
|
504
|
+
|
|
505
|
+
def _format_code_snippets(self, files) -> str:
|
|
506
|
+
"""Format cached code files as markdown for system prompt."""
|
|
507
|
+
snippets = "\n\n## Cached Code (from Raysurfer)\n\n"
|
|
508
|
+
snippets += "The following pre-validated code is available for this task. "
|
|
509
|
+
snippets += "Use it directly or adapt it as needed.\n\n"
|
|
510
|
+
|
|
511
|
+
for f in files:
|
|
512
|
+
snippets += f"### {f.filename}\n"
|
|
513
|
+
snippets += f"**Description**: {f.description}\n"
|
|
514
|
+
snippets += f"**Entrypoint**: `{f.entrypoint}`\n"
|
|
515
|
+
snippets += f"**Confidence**: {f.verdict_score:.0%}\n\n"
|
|
516
|
+
snippets += f"```{f.language}\n{f.source}\n```\n\n"
|
|
517
|
+
|
|
518
|
+
return snippets
|
|
519
|
+
|
|
520
|
+
async def _upload_to_cache(self) -> None:
|
|
521
|
+
"""Upload generated code to the Raysurfer cache."""
|
|
522
|
+
if not self._raysurfer or not self._current_query:
|
|
523
|
+
return
|
|
524
|
+
|
|
525
|
+
try:
|
|
526
|
+
result = await self._raysurfer.submit_execution_result(
|
|
527
|
+
task=self._current_query,
|
|
528
|
+
files_written=self._generated_files,
|
|
529
|
+
succeeded=self._task_succeeded,
|
|
530
|
+
)
|
|
531
|
+
if result.code_blocks_stored > 0:
|
|
532
|
+
logger.info(f"Cached {result.code_blocks_stored} code blocks")
|
|
533
|
+
except Exception as e:
|
|
534
|
+
logger.debug(f"Failed to upload to cache: {e}")
|
|
535
|
+
|
|
536
|
+
async def _submit_votes(self) -> None:
|
|
537
|
+
"""Submit thumbs up votes for cached code blocks that helped complete the task."""
|
|
538
|
+
if not self._raysurfer or not self._current_query:
|
|
539
|
+
return
|
|
540
|
+
|
|
541
|
+
for block in self._cached_code_blocks:
|
|
542
|
+
try:
|
|
543
|
+
await self._raysurfer.record_cache_usage(
|
|
544
|
+
task=self._current_query,
|
|
545
|
+
code_block_id=block["code_block_id"],
|
|
546
|
+
code_block_name=block["filename"],
|
|
547
|
+
code_block_description=block["description"],
|
|
548
|
+
succeeded=self._task_succeeded,
|
|
549
|
+
)
|
|
550
|
+
logger.debug(f"Submitted vote for {block['filename']}")
|
|
551
|
+
except Exception as e:
|
|
552
|
+
logger.debug(f"Failed to submit vote for {block['filename']}: {e}")
|
raysurfer/sdk_types.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Types for Claude Agent SDK integration"""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CodeFile(BaseModel):
|
|
9
|
+
"""A code file ready to be written to sandbox"""
|
|
10
|
+
|
|
11
|
+
code_block_id: str
|
|
12
|
+
filename: str # e.g., "github_fetcher.py"
|
|
13
|
+
source: str # Full source code
|
|
14
|
+
entrypoint: str # Function to call
|
|
15
|
+
description: str
|
|
16
|
+
input_schema: dict[str, Any] = Field(default_factory=dict)
|
|
17
|
+
output_schema: dict[str, Any] = Field(default_factory=dict)
|
|
18
|
+
language: str
|
|
19
|
+
dependencies: list[str] = Field(default_factory=list)
|
|
20
|
+
verdict_score: float
|
|
21
|
+
thumbs_up: int
|
|
22
|
+
thumbs_down: int
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GetCodeFilesResponse(BaseModel):
|
|
26
|
+
"""Response with code files for a task"""
|
|
27
|
+
|
|
28
|
+
files: list[CodeFile]
|
|
29
|
+
task: str
|
|
30
|
+
total_found: int
|