-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathagentlib.py
More file actions
147 lines (117 loc) · 4.98 KB
/
Copy pathagentlib.py
File metadata and controls
147 lines (117 loc) · 4.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
"""Thin wrapper around the Cursor Python SDK shared by bootstrap.py and
orchestrator.py.
Keeps the SDK-specific concerns in one place: optional import with a friendly
message, API-key resolution, a persistent multi-turn agent context manager, and
a ``send + stream + wait`` helper that distinguishes the two failure modes the
SDK skill warns about (startup failure vs run failure).
"""
from __future__ import annotations
import os
import sys
from contextlib import contextmanager
from typing import Iterator, Optional
DEFAULT_MODEL = os.environ.get("AUTO_RL_AGENT_MODEL", "composer-2.5")
class AgentUnavailable(RuntimeError):
"""Raised when the SDK or API key is missing; carries guidance text."""
_TOKENS_HARDENED = False
def _harden_sdk_tokens() -> None:
"""Work around a cursor-sdk bridge bug.
The vendored node bridge's arg parser rejects any flag value that starts with
"-" (treats it as a missing value). But the SDK mints auth tokens with
``secrets.token_urlsafe(32)``, which starts with "-" ~1.5% of the time,
intermittently crashing bridge launch with
"Missing value for --tool-callback-auth-token". We replace the token
generator with one that never produces a leading "-".
"""
global _TOKENS_HARDENED
if _TOKENS_HARDENED:
return
import secrets
def _safe_token() -> str:
tok = secrets.token_urlsafe(32)
return ("a" + tok[1:]) if tok[:1] == "-" else tok
for mod_name in ("cursor_sdk._tool_callback", "cursor_sdk._store_callback"):
try:
mod = __import__(mod_name, fromlist=["_new_auth_token"])
except Exception:
continue
if hasattr(mod, "_new_auth_token"):
mod._new_auth_token = _safe_token
_TOKENS_HARDENED = True
def _import_sdk():
try:
import cursor_sdk # noqa: F401
except Exception as exc: # pragma: no cover - environment dependent
raise AgentUnavailable(
"The Cursor Python SDK is not installed. Install it with:\n"
" uv pip install cursor-sdk (or) pip install cursor-sdk\n"
f"(import error: {exc})"
) from exc
_harden_sdk_tokens()
from cursor_sdk import Agent, CursorAgentError, LocalAgentOptions
return Agent, CursorAgentError, LocalAgentOptions
def require_api_key(explicit: Optional[str] = None) -> str:
key = explicit or os.environ.get("CURSOR_API_KEY")
if not key:
raise AgentUnavailable(
"CURSOR_API_KEY is not set. Get a key at "
"https://cursor.com/dashboard/integrations and run:\n"
' export CURSOR_API_KEY="cursor_..."'
)
return key.strip()
def _message_text(message) -> str:
"""Best-effort extraction of assistant text from an SDK stream message."""
if getattr(message, "type", None) != "assistant":
return ""
inner = getattr(message, "message", None)
content = getattr(inner, "content", None) if inner is not None else None
if not content:
return ""
out = []
for block in content:
if getattr(block, "type", None) == "text":
out.append(getattr(block, "text", ""))
return "".join(out)
@contextmanager
def open_agent(cwd, model: str = DEFAULT_MODEL, api_key: Optional[str] = None) -> Iterator["AgentSession"]:
"""Open a persistent local agent rooted at ``cwd`` for multi-turn use."""
Agent, CursorAgentError, LocalAgentOptions = _import_sdk()
key = require_api_key(api_key)
with Agent.create(
model=model,
api_key=key,
local=LocalAgentOptions(cwd=str(cwd)),
) as agent:
yield AgentSession(agent, CursorAgentError)
class AgentSession:
"""Wraps a live agent; ``send`` streams output and returns the RunResult."""
def __init__(self, agent, cursor_agent_error):
self._agent = agent
self._CursorAgentError = cursor_agent_error
@property
def agent_id(self) -> str:
return getattr(self._agent, "agent_id", "?")
def send(self, prompt: str, *, label: str = "", echo: bool = True):
try:
run = self._agent.send(prompt)
except self._CursorAgentError as exc: # did not start
raise AgentUnavailable(
f"agent run failed to start (auth/config/network): {exc}"
) from exc
run_id = getattr(run, "id", "?")
print(f"[agent] {label} run={run_id} agent={self.agent_id}", file=sys.stderr)
try:
for message in run.messages():
if echo:
text = _message_text(message)
if text:
print(text, end="", flush=True)
except self._CursorAgentError as exc: # connection dropped mid-stream
raise AgentUnavailable(f"agent stream failed: {exc}") from exc
if echo:
print()
result = run.wait()
return result
def result_failed(result) -> bool:
"""True if a *started* run ended in error (vs the finished happy path)."""
return getattr(result, "status", None) == "error"