Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f0b2c6a
Integrate Harness APIs
SSharma-10 Jun 23, 2026
30bfebd
lint fix
SSharma-10 Jun 23, 2026
88c937a
Merge branch 'main' into OHS_endpoints
SSharma-10 Jun 24, 2026
28ebe11
Merge remote-tracking branch 'origin/main' into OHS_endpoints
SSharma-10 Jun 26, 2026
1913922
Managed Agents: spec-based session creation + high-level AgentSession…
logwolvy Jun 26, 2026
0ea7621
add workspace file upload/download endpoints
SSharma-10 Jun 30, 2026
42c74c3
lint fix
SSharma-10 Jun 30, 2026
5e5eed6
add support for session name
SSharma-10 Jul 3, 2026
64e0478
Merge branch 'main' into OHS_endpoints
SSharma-10 Jul 3, 2026
e6c31fa
Merge branch 'main' into OHS_endpoints
SSharma-10 Jul 3, 2026
d6cc1ff
add support for pause/resume
SSharma-10 Jul 3, 2026
2410a2b
examples/agents: add policy engine test scripts
vhrahulkumar Jul 6, 2026
d8c8033
Add action gateway SDK functions
tgillam-do Jul 8, 2026
89a2d1e
Add DO Action Gateway MCP server SDK functions
tgillam-do Jul 8, 2026
3a853e6
Update acton gateway tests
tgillam-do Jul 8, 2026
301a202
Fix action gateway session URLs
tgillam-do Jul 20, 2026
7ad0ebc
Fix test lint formatting
tgillam-do Jul 21, 2026
5bc5ea7
Fix action gateway session route
tgillam-do Jul 21, 2026
d73fcf0
Finalize action gateway SDK compatibility
tgillam-do Jul 21, 2026
2b4ed3f
Merge branch 'main' into OHS_endpoints
SSharma-10 Jul 27, 2026
00fef69
fix
tgillam-do Jul 27, 2026
22f1d47
update
tgillam-do Jul 27, 2026
6e82c71
update
tgillam-do Jul 27, 2026
d5bd2f2
approval flow
tgillam-do Jul 27, 2026
0d6784e
add support to transfer large size files
SSharma-10 Jul 28, 2026
f4362ef
test fix
SSharma-10 Jul 28, 2026
6a57a30
update
tgillam-do Jul 29, 2026
5efe1e2
remove doc
tgillam-do Jul 29, 2026
72bd5d0
Generate Action Gateway public API clients
tgillam-do Jul 29, 2026
9a991e7
Use generated Action Gateway public routes
tgillam-do Jul 29, 2026
43ce9a4
Add generated Action Gateway API examples
tgillam-do Jul 29, 2026
89f3111
Fix Action Gateway test lint
tgillam-do Jul 29, 2026
dbca478
Test generated Action Gateway session delegation
tgillam-do Jul 29, 2026
52b1e1e
Handle flat toolbelt create responses
tgillam-do Jul 29, 2026
5ec43b5
add support for webhooks and triggers
SSharma-10 Jul 30, 2026
5036c88
lint fix
SSharma-10 Jul 30, 2026
15cdcfa
Merge branch 'add-action-gateway' into OHS_endpoints
SSharma-10 Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ clean: ## Removes all generated code (except _patch.py files)
@printf "=== Cleaning src directory\n"
@rm -rf src/pydo/resources
@rm -rf src/pydo/types
@find src/pydo -type f ! -name "_patch.py" ! -name "custom_*.py" ! -name "exceptions.py" -exec rm -rf {} +
@find src/pydo -type f \
! -name "_patch.py" ! -name "custom_*.py" ! -name "exceptions.py" \
! -path "*/agents/__init__.py" ! -path "*/aio/agents/__init__.py" \
! -path "*/agents/session.py" ! -path "*/aio/agents/session.py" \
-exec rm -rf {} +

.PHONY: download-spec
download-spec: ## Download Latest DO Spec
Expand Down
24 changes: 24 additions & 0 deletions examples/agents/async_stream_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Async stream session events. Set DIGITALOCEAN_TOKEN and SESSION_ID."""

import asyncio
import os

from pydo.aio import Client

SESSION_ID = os.environ["SESSION_ID"]


async def main() -> None:
async with Client(token=os.environ["DIGITALOCEAN_TOKEN"]) as client:
stream = await client.agents.sessions.stream(SESSION_ID)
async with stream as events:
async for event in events:
if getattr(event, "type", None) == "run.token_delta" and event.get("data"):
print(event.data.text, end="", flush=True)
elif "token_chunk" in event:
print(event.token_chunk.text, end="", flush=True)
else:
print(event)


asyncio.run(main())
53 changes: 53 additions & 0 deletions examples/agents/attach.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Attach to an EXISTING agent session and stream one turn.

Like ``doctl agents attach``: connect to a session that already exists, send a
prompt, and consume the SSE event feed through the high-level API — no thread,
no raw event-string matching. attach() never destroys the session, so it stays
alive for further turns.

Get a SESSION_ID first by creating one (the session stays up):
AGENT_SPEC=... python examples/agents/create_session.py
then copy the "session_id" it prints.

Required env:
DIGITALOCEAN_TOKEN
PYDO_AGENTS_ENDPOINT stage2: https://api.s2r1.internal.digitalocean.com
SESSION_ID the session to attach to

Optional env:
PROMPT message to send (default below)
"""

import os
import sys

from pydo import Client
from pydo.agents import AgentEventType

client = Client(
token=os.environ["DIGITALOCEAN_TOKEN"],
agents_endpoint=os.environ.get("PYDO_AGENTS_ENDPOINT"),
)

agent = client.agents.attach(os.environ["SESSION_ID"])
prompt = os.environ.get("PROMPT", "In one short sentence, what is DigitalOcean?")

print(f"[attached {agent.session_id}]\n>>> {prompt}\n", file=sys.stderr)

stream = agent.run_streamed(prompt) # opens the stream, sends input, yields events
for event in stream:
if event.type == AgentEventType.TOKEN:
print(event.text, end="", flush=True) # live reply -> stdout
elif event.type == AgentEventType.TOOL_CALL:
print(f"\n[tool] {event.tool_name}", file=sys.stderr)
elif event.type == AgentEventType.HITL_REQUESTED:
print(f"\n[hitl auto-approved] {event.request_id}", file=sys.stderr)

result = stream.result
print(
f"\n\n[{result.status}] captured {len(result.final_output)} chars "
f"(tokens out={result.usage.get('tokens_out')})",
file=sys.stderr,
)
# attach() leaves the session running; destroy it when done:
# SESSION_ID=... python examples/agents/destroy_session.py
26 changes: 26 additions & 0 deletions examples/agents/create_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Create a session from an agent manifest (``agents.yaml``).

A session is created entirely from the agent spec — the runtime adapter,
sandbox template, env vars, etc. are all defined in the manifest. The client
uploads it verbatim (``Content-Type: application/x-yaml``); the server parses
and validates it.

Required env:
DIGITALOCEAN_TOKEN
PYDO_AGENTS_ENDPOINT (stage2: https://api.s2r1.internal.digitalocean.com)
AGENT_SPEC path to the agent spec YAML (default: agent-spec.yaml)
"""

import json
import os

from pydo import Client

spec_path = os.environ.get("AGENT_SPEC", "agent-spec.yaml")
with open(spec_path, "r", encoding="utf-8") as fh:
manifest = fh.read()

client = Client(token=os.environ["DIGITALOCEAN_TOKEN"])
resp = client.agents.sessions.create_from_manifest(manifest)

print(json.dumps(resp, indent=2, default=str))
12 changes: 12 additions & 0 deletions examples/agents/destroy_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Destroy a session. Set DIGITALOCEAN_TOKEN and SESSION_ID."""

import os

from pydo import Client

SESSION_ID = os.environ["SESSION_ID"]

client = Client(token=os.environ["DIGITALOCEAN_TOKEN"])
client.agents.sessions.destroy(SESSION_ID)

print("ok", SESSION_ID)
13 changes: 13 additions & 0 deletions examples/agents/get_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Get a session. Set DIGITALOCEAN_TOKEN and SESSION_ID."""

import json
import os

from pydo import Client

SESSION_ID = os.environ["SESSION_ID"]

client = Client(token=os.environ["DIGITALOCEAN_TOKEN"])
resp = client.agents.sessions.get(SESSION_ID)

print(json.dumps(resp, indent=2, default=str))
11 changes: 11 additions & 0 deletions examples/agents/list_sessions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""List sessions. Set DIGITALOCEAN_TOKEN (and PYDO_AGENTS_ENDPOINT for stage2)."""

import os

from pydo import Client

client = Client(token=os.environ["DIGITALOCEAN_TOKEN"])

resp = client.agents.sessions.list(page_size=50)
for session in resp.get("sessions", []):
print(session.session_id, session.status, session.agent_kind)
19 changes: 19 additions & 0 deletions examples/agents/resolve_hitl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Resolve HITL. Set DIGITALOCEAN_TOKEN, SESSION_ID, REQUEST_ID, and PYDO_AGENTS_ENDPOINT (stage2)."""

import os

from pydo import Client
from pydo.agents import HITLOutcome, ResolutionSource

SESSION_ID = os.environ["SESSION_ID"]
REQUEST_ID = os.environ["REQUEST_ID"]

client = Client(token=os.environ["DIGITALOCEAN_TOKEN"])
client.agents.sessions.resolve_hitl(
SESSION_ID,
REQUEST_ID,
outcome=HITLOutcome.APPROVE,
source=ResolutionSource.OUT_OF_BAND,
)

print("ok", REQUEST_ID)
31 changes: 31 additions & 0 deletions examples/agents/run_blocking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""One-liner blocking run: create from spec -> run -> print -> destroy.

The high-level API collapses the whole flow into ``agent.run(prompt)``, which
blocks until the run finishes and returns the assembled output. The ``with``
block auto-destroys the session on exit.

Required env:
DIGITALOCEAN_TOKEN
PYDO_AGENTS_ENDPOINT stage2: https://api.s2r1.internal.digitalocean.com
AGENT_SPEC path to the agent spec YAML

Optional env:
PROMPT
"""

import os

from pydo import Client

client = Client(
token=os.environ["DIGITALOCEAN_TOKEN"],
agents_endpoint=os.environ.get("PYDO_AGENTS_ENDPOINT"),
)

with open(os.environ.get("AGENT_SPEC", "agent-spec.yaml"), encoding="utf-8") as fh:
manifest = fh.read()

prompt = os.environ.get("PROMPT", "In one short sentence, what is DigitalOcean?")

with client.agents.start(manifest) as agent: # auto-destroys on exit
print(agent.run(prompt).final_output)
14 changes: 14 additions & 0 deletions examples/agents/send_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Send input to a session. Set DIGITALOCEAN_TOKEN and SESSION_ID."""

import json
import os

from pydo import Client

SESSION_ID = os.environ["SESSION_ID"]
TEXT = "Summarise the README in two sentences."

client = Client(token=os.environ["DIGITALOCEAN_TOKEN"])
resp = client.agents.sessions.send_input(SESSION_ID, text=TEXT)

print(json.dumps(resp, indent=2, default=str))
70 changes: 70 additions & 0 deletions examples/agents/session_e2e.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""End-to-end hosted-agents demo: create -> attach -> destroy.

Uses pydo's high-level agent interface, so consuming the SSE feed needs no
manual wiring — no background thread, no completion event, no dispatching on
raw ``run.*`` event strings, and no explicit teardown:

* ``client.agents.start(manifest)`` creates the session and returns a handle
that auto-destroys when the ``with`` block exits.
* ``agent.run_streamed(prompt)`` opens the stream, sends the prompt, and
yields normalized, typed events (auto-approving HITL prompts by default).
* ``agent.run(prompt)`` is the blocking one-liner equivalent.

Required env:
DIGITALOCEAN_TOKEN
PYDO_AGENTS_ENDPOINT stage2: https://api.s2r1.internal.digitalocean.com
AGENT_SPEC path to the agent spec YAML (agents.yaml manifest)

Optional env:
PROMPT message to send (default: a short demo prompt)
"""

import os
import sys

from pydo import Client
from pydo.agents import AgentEventType


def main():
spec_path = os.environ.get("AGENT_SPEC", "agent-spec.yaml")
with open(spec_path, "r", encoding="utf-8") as fh:
manifest = fh.read()

prompt = os.environ.get("PROMPT", "In one short sentence, what is DigitalOcean?")

client = Client(
token=os.environ["DIGITALOCEAN_TOKEN"],
agents_endpoint=os.environ.get("PYDO_AGENTS_ENDPOINT"),
)

# create + auto-destroy via the context manager.
with client.agents.start(manifest) as agent:
print(f"[session {agent.session_id} | {agent.status}]", file=sys.stderr)
print(f">>> {prompt}\n", file=sys.stderr)

# attach: send the prompt and consume typed events as they stream in.
stream = agent.run_streamed(prompt)
for event in stream:
if event.type == AgentEventType.TOKEN:
print(event.text, end="", flush=True) # live reply -> stdout
elif event.type == AgentEventType.TOOL_CALL:
print(f"\n[tool] {event.tool_name}", file=sys.stderr)
elif event.type == AgentEventType.HITL_REQUESTED:
print(f"\n[hitl auto-approved] {event.request_id}", file=sys.stderr)

result = stream.result
print(
f"\n\n[{result.status}] "
f"tokens in={result.usage.get('tokens_in')} "
f"out={result.usage.get('tokens_out')} "
f"| captured {len(result.final_output)} chars",
file=sys.stderr,
)

# session is destroyed here.
return 0 if stream.status == "completed" else 1


if __name__ == "__main__":
raise SystemExit(main())
18 changes: 18 additions & 0 deletions examples/agents/start_oauth_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Start GitHub OAuth. Set DIGITALOCEAN_TOKEN and SESSION_ID."""

import json
import os

from pydo import Client
from pydo.agents import OAuthProvider

SESSION_ID = os.environ["SESSION_ID"]

client = Client(token=os.environ["DIGITALOCEAN_TOKEN"])
resp = client.agents.sessions.start_oauth_flow(
SESSION_ID,
OAuthProvider.GITHUB,
requested_scopes=["repo"],
)

print(json.dumps(resp, indent=2, default=str))
31 changes: 31 additions & 0 deletions examples/agents/stream_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Stream session events.

Required env:
DIGITALOCEAN_TOKEN
SESSION_ID
PYDO_AGENTS_ENDPOINT (stage2: https://api.s2r1.internal.digitalocean.com)

Tip: open this stream *before* send_input.py so you catch live events.
If the run already finished, the stream may sit idle until the next input.
"""

import os
import sys

from pydo import Client

SESSION_ID = os.environ["SESSION_ID"]

client = Client(token=os.environ["DIGITALOCEAN_TOKEN"])
print(f"agents endpoint: {client.agents.base_url}", file=sys.stderr)

with client.agents.sessions.stream(SESSION_ID) as events:
for event in events:
# SPI wire (harness-api HTTP handler): type + data.text
if getattr(event, "type", None) == "run.token_delta" and event.get("data"):
print(event.data.text, end="", flush=True)
# grpc-gateway proto envelope (legacy)
elif "token_chunk" in event:
print(event.token_chunk.text, end="", flush=True)
else:
print(event)
10 changes: 10 additions & 0 deletions src/pydo/_patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ class Client( # type: ignore
subdomain (e.g. ``"https://<id>.agents.do-ai.run"``).
Required only when using agent inference endpoints.
:paramtype agent_endpoint: str
:keyword agents_endpoint: Hosted Agents API base URL (default
``api.digitalocean.com``; override via ``PYDO_AGENTS_ENDPOINT``).
"""

def __init__(
Expand All @@ -68,6 +70,7 @@ def __init__(
timeout: int = 120,
inference_endpoint: str = INFERENCE_BASE_URL,
agent_endpoint: str = "",
agents_endpoint: Optional[str] = None,
**kwargs,
):
if token is not None and api_key is not None:
Expand Down Expand Up @@ -111,6 +114,13 @@ def __init__(
self.images.generate = inference_images.generate
self.images.generations = inference_images.generations

try:
from pydo.agents import AgentsResources
except ImportError:
self.agents = None
else:
self.agents = AgentsResources(self, agents_endpoint=agents_endpoint)

def _setup_inference_routing(
self,
inference_endpoint: str,
Expand Down
Loading
Loading