From faebe81cd282b041e9a34fecae417b4d051b52d7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 31 Aug 2025 11:49:18 +0000 Subject: [PATCH 1/3] Update docs: explain Runtime.start() blocking behavior across environments Co-authored-by: nivedit --- README.md | 9 +++++++++ docs/docs/getting-started.md | 33 +++++++++++++++++++++++++++++++++ python-sdk/README.md | 30 ++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/README.md b/README.md index 0ad5a0bc..ec773b7f 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,15 @@ This allows developers to deploy production agents that can scale beautifully to ).start() ``` + Note: `Runtime.start()` will block the main thread in normal scripts (no running event loop). In interactive environments with an active loop (e.g., Jupyter), it returns an `asyncio.Task` and does not block. For non-blocking usage from a sync script, you can run it in a background thread: + + ```python + from threading import Thread + + runtime = Runtime(name="my-first-runtime", namespace="hello-world", nodes=[MyFirstNode]) + Thread(target=runtime.start, daemon=True).start() + ``` + - ### Define your first graph Graphs are then described connecting nodes with relationships in json objects. Exosphere runs graph as per defined trigger conditions. See [Graph definitions](https://docs.exosphere.host/exosphere/create-graph/) to see more examples. diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md index 7ab29670..a668a902 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/getting-started.md @@ -86,6 +86,39 @@ Runtime( ).start() ``` +### Note on blocking behavior of `Runtime.start()` + +By design, `Runtime.start()` runs the runtime loop indefinitely and will block the main thread when no asyncio event loop is running (e.g., normal Python scripts). In interactive environments that already have an event loop (like Jupyter notebooks), `Runtime.start()` returns an `asyncio.Task` and does not block. + +- If you're in an async/interactive environment (e.g., Jupyter/REPL with a running loop): + + ```python + # Jupyter/async environment + runtime = Runtime(namespace="MyProject", name="DataProcessor", nodes=[SampleNode]) + task = runtime.start() # task is an asyncio.Task running in the background + # You can continue interacting, and optionally await/cancel the task later + # await task # if you want to wait on it + ``` + +- If you need a non-blocking start from a regular sync script, run it in a background thread: + + ```python + from threading import Thread + + runtime = Runtime(namespace="MyProject", name="DataProcessor", nodes=[SampleNode]) + Thread(target=runtime.start, daemon=True).start() + # continue with other work in the main thread + ``` + +- Alternatively, from an async context you can offload to a thread: + + ```python + import asyncio + + runtime = Runtime(namespace="MyProject", name="DataProcessor", nodes=[SampleNode]) + await asyncio.to_thread(runtime.start) + ``` + ## Next Steps Now that you have the basics, explore: diff --git a/python-sdk/README.md b/python-sdk/README.md index 7df6c3c2..c7f68168 100644 --- a/python-sdk/README.md +++ b/python-sdk/README.md @@ -59,6 +59,36 @@ Runtime( ).start() ``` +### Note on blocking behavior of `Runtime.start()` + +`Runtime.start()` blocks the main thread when no asyncio event loop is running (typical Python scripts). In environments with an active event loop (e.g., Jupyter), it returns an `asyncio.Task` and does not block. + +- Jupyter/interactive (non-blocking): + + ```python + runtime = Runtime(namespace="MyProject", name="DataProcessor", nodes=[SampleNode]) + task = runtime.start() # background asyncio.Task + # await task # optionally wait on it later + ``` + +- Regular sync script (non-blocking via thread): + + ```python + from threading import Thread + + runtime = Runtime(namespace="MyProject", name="DataProcessor", nodes=[SampleNode]) + Thread(target=runtime.start, daemon=True).start() + ``` + +- From async code (offload to a thread): + + ```python + import asyncio + + runtime = Runtime(namespace="MyProject", name="DataProcessor", nodes=[SampleNode]) + await asyncio.to_thread(runtime.start) + ``` + ## Environment Configuration The SDK requires the following environment variables for authentication with ExosphereHost: From c52ebb01d5483c183b1f4144ce7dd42959a78280 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 31 Aug 2025 11:53:44 +0000 Subject: [PATCH 2/3] Add script to create GitHub pull requests programmatically Co-authored-by: nivedit --- .tmp_create_pr.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .tmp_create_pr.py diff --git a/.tmp_create_pr.py b/.tmp_create_pr.py new file mode 100644 index 00000000..e32272e4 --- /dev/null +++ b/.tmp_create_pr.py @@ -0,0 +1,46 @@ +import subprocess, json, re, sys, urllib.request + +def get_owner_repo_and_token(): + url = subprocess.check_output(["git","remote","get-url","origin"]).decode().strip() + m = re.search(r"https://x-access-token:([^@]+)@github.com/([^/]+/[^/.]+)(?:\\.git)?$", url) + if not m: + return None, None + token = m.group(1) + owner_repo = m.group(2) + return owner_repo, token + +def get_current_branch(): + return subprocess.check_output(["git","rev-parse","--abbrev-ref","HEAD"]).decode().strip() + +owner_repo, token = get_owner_repo_and_token() +branch = get_current_branch() +if not owner_repo or not token: + print("") + sys.exit(0) + +payload = { + "title": "docs: document Runtime.start() blocking behavior (#280)", + "head": branch, + "base": "main", + "body": "This PR documents Runtime.start() blocking behavior and adds non-blocking examples. Fixes #280." +} + +data = json.dumps(payload).encode() +req = urllib.request.Request( + f"https://api.github.com/repos/{owner_repo}/pulls", + data=data, + headers={ + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "X-GitHub-Api-Version": "2022-11-28", + }, + method="POST", +) +try: + with urllib.request.urlopen(req) as resp: + pr = json.loads(resp.read().decode()) + print(pr.get("html_url", "")) +except Exception as e: + # On failure, print empty to fallback to compare URL construction outside + print("") From e84014c0b6172cb7a10de3ccf1eb3860ee771c14 Mon Sep 17 00:00:00 2001 From: Nivedit Jain Date: Sun, 31 Aug 2025 17:25:06 +0530 Subject: [PATCH 3/3] Delete .tmp_create_pr.py --- .tmp_create_pr.py | 46 ---------------------------------------------- 1 file changed, 46 deletions(-) delete mode 100644 .tmp_create_pr.py diff --git a/.tmp_create_pr.py b/.tmp_create_pr.py deleted file mode 100644 index e32272e4..00000000 --- a/.tmp_create_pr.py +++ /dev/null @@ -1,46 +0,0 @@ -import subprocess, json, re, sys, urllib.request - -def get_owner_repo_and_token(): - url = subprocess.check_output(["git","remote","get-url","origin"]).decode().strip() - m = re.search(r"https://x-access-token:([^@]+)@github.com/([^/]+/[^/.]+)(?:\\.git)?$", url) - if not m: - return None, None - token = m.group(1) - owner_repo = m.group(2) - return owner_repo, token - -def get_current_branch(): - return subprocess.check_output(["git","rev-parse","--abbrev-ref","HEAD"]).decode().strip() - -owner_repo, token = get_owner_repo_and_token() -branch = get_current_branch() -if not owner_repo or not token: - print("") - sys.exit(0) - -payload = { - "title": "docs: document Runtime.start() blocking behavior (#280)", - "head": branch, - "base": "main", - "body": "This PR documents Runtime.start() blocking behavior and adds non-blocking examples. Fixes #280." -} - -data = json.dumps(payload).encode() -req = urllib.request.Request( - f"https://api.github.com/repos/{owner_repo}/pulls", - data=data, - headers={ - "Accept": "application/vnd.github+json", - "Authorization": f"Bearer {token}", - "Content-Type": "application/json", - "X-GitHub-Api-Version": "2022-11-28", - }, - method="POST", -) -try: - with urllib.request.urlopen(req) as resp: - pr = json.loads(resp.read().decode()) - print(pr.get("html_url", "")) -except Exception as e: - # On failure, print empty to fallback to compare URL construction outside - print("")