The Python package is arranged symmetrically: the same concepts, the same lifecycle, the same security boundaries. The differences are purely linguistic - synchronous and asynchronous clients, typed data structures and ordinary iteration over streams and pages. A modern language version is required, and the execution semantics match the other package: local files stay on the machine, reasoning goes through hosted models, and automatic mode requires explicitly limiting the tools. That match is not an accident of implementation but a stated property: the two packages describe the same service, and they should differ in the shape of the call rather than in what is allowed at all.
It helps to see a minimal integration once. Below are creating an agent with a model, a key from the environment and local options through a context manager, sending a prompt and printing the result. The context manager is not decoration here: it guarantees that resources are released on any exit from the block, an exception included, and in a long-lived service it is precisely the agents forgotten after errors that become a source of unexplained spend.
The package also accepts plain dictionaries, but typed structures are better for two reasons. The first is practical: editor hints and a type check catch a typo in a parameter name before the run rather than at the moment when the service has already worked an hour with the wrong setting. The second is strategic: when the schema changes in a new version, typed code fails at check time while a dictionary silently passes an unknown field along, and you learn about the change from altered behavior rather than from an error. For an integration that lives a year, that is the difference between a managed upgrade and a sudden breakage.
Hence a set of operational rules, the same for both languages. The package version is pinned. After an upgrade the type check and tests are run. The key is not stored in sources and not assembled into a dictionary that later lands whole in a log during error triage. Those are dull items, but they are exactly what separates an integration you can hand to a colleague from a script that works only for its author and only on his machine.
The core idea here is broader than the language. If you have executors in two languages, they must share one contract: the same way of discovering available models, the same prompt schema, the same permission control points, the same timeouts and cancellation, the same event storage and the same spend metrics. Then the packages differ by runtime rather than by level of security, and the answer to what exactly the automation did yesterday does not depend on what it was written in.
Without such a contract a familiar situation arises: in one language the executor works in a sandbox and writes metrics, and in the other it does not, because it was written by a different person at a different time and in a hurry. The difference is discovered at the moment of an incident, when it turns out that half the automation lacked limits everyone assumed were shared. Worse, the divergence is nearly impossible to notice in advance: both executors run, both produce results, and the difference shows only in what they do not do.
The choice between the synchronous and the asynchronous client looks like a matter of taste, but it has a price. The synchronous one is simpler and fits scripts and one-off tasks where nobody expects concurrency. The asynchronous one is needed by a service holding several runs at once: a run is a minutes-long operation, and a thread waiting on an event stream does nothing all that time. The danger appears when they are mixed: a single blocking synchronous call inside an asynchronous loop stops the whole loop rather than just its own task. The symptom of that mistake is recognizable: throughput falls to one task at a time while the processor idles, and the logs show neither errors nor timeouts. So the client is chosen to match the execution model rather than out of habit.
The contract is best expressed not as a document but as code - a thin shared wrapper in each language. It takes on exactly six things: obtaining the key from the secret store, discovering available models, assembling the request against a common schema, permission control points, timeouts with cancellation, and recording events and spend metrics. Everything else stays with the package. The boundary matters here: the wrapper must not restate the programmatic interface in its own words, or it will need maintenance at every upgrade. Its job is to pin the policy in one place, so that a new executor inherits the rules by the fact of using it rather than by its author's diligence.
The engineering conclusion is simple: the language is an implementation detail while the contract is a matter of agreement. Describe it once as a shared wrapper library, and moving between languages will stop being a source of differences in behavior and in security.
The typical failures are predictable. Assembling the configuration as a dictionary and losing typing along with early error checking. Not pinning the package version and getting different behavior on different machines. Leaving the key in a source file or in a dictionary that lands in a log. Mixing blocking calls with asynchronous execution and losing concurrency. And running two languages with different security rules instead of one shared contract.
import os
from cursor_sdk import Agent, LocalAgentOptions
with Agent.create(
model="composer-2.5",
api_key=os.environ["CURSOR_API_KEY"],
local=LocalAgentOptions(cwd=os.getcwd()),
) as agent:
result = agent.send("Explain the current test architecture")
print(result.text())
# the context manager releases resources; typed options catch schema changes