Skip to content

Jaden/sdk panel - #28

Merged
jadenfix merged 15 commits into
mainfrom
jaden/sdk-panel
May 5, 2026
Merged

jadenfix merged 15 commits into
mainfrom
jaden/sdk-panel

Conversation

@jadenfix

@jadenfix jadenfix commented May 5, 2026

Copy link
Copy Markdown
Contributor

No description provided.

jadenfix and others added 15 commits April 29, 2026 00:16
…ponse

Step 1 of the SDK migration to the generated raw client. The retry policy
(502/503/504, idempotent methods only, exponential backoff) moves from
RoeHTTPClient._request_with_retry into a custom RoeRetryTransport on the
shared httpx.Client — so every request, including raw-client calls, gets
the same budget. Lifts the dynamic-input multipart classifier into
roe.utils.inputs and adds translate_response() to roe.exceptions for
status-code → typed exception mapping at the wrapper boundary. No public
API changes; RoeHTTPClient stays in place pending the agents migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 2 of the SDK migration. PoliciesAPI and PolicyVersionsAPI now call
generated endpoints in roe._generated.api.v1 directly, returning the
generated response models (Policy, PolicyVersion, PaginatedPolicyList,
PaginatedPolicyVersionList, CreatePolicy) instead of the hand-written
pydantic wrappers. translate_response() at the wrapper boundary maps
non-2xx responses to the existing RoeAPIException family. RawClient is
constructed with raise_on_unexpected_status=False so we own the error
path.

BREAKING: list/retrieve/create/update return generated attrs models
rather than pydantic models — field access by name still works but
isinstance(x, roe.models.policy.Policy) does not. The hand-written
roe.models.policy.* classes remain unused; cleanup is a follow-up.

PolicyVersionsAPI.list now also accepts page/page_size and returns a
PaginatedPolicyVersionList instead of a flat list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites every method on AgentsAPI, AgentVersionsAPI, and AgentJobsAPI to
delegate to the generated endpoints under roe._generated.api.v1 (and v2
where applicable). JSON endpoints use sync_detailed; multipart endpoints
(run / run_sync / run_version / run_version_sync) bypass the broken
generated to_multipart() via a new utils._dynamic_call.call_dynamic that
mutates _get_kwargs() output to inject our own (data, files) tuple.
download_reference uses the same _get_kwargs path to grab raw bytes.

Job and JobBatch are kept as the SDK's stateful polling helpers but now
return the generated AgentJobResultResponse / AgentJobResultItem types
directly. Terminal status + error_message land in the result's
additional_properties dict so wait() callers can still inspect them.
JobStatus is relocated to roe.models.job; the hand-written
roe.models.responses module survives temporarily for the cleanup pass
but is no longer re-exported from the public roe package.

The update() endpoints (AgentsAPI.update / AgentVersionsAPI.update)
switch from PUT to PATCH (v1_agents_partial_update,
v1_agents_versions_partial_update) so partial-update ergonomics keep
working without the generated PUT request body's all-fields-required
constraint. AgentsAPI's constructor now takes the generated
AuthenticatedClient instead of RoeHTTPClient.

This is a major-bump breaking change: the public surface preserved is
RoeClient + agents/policies method names, Job/JobBatch/FileUpload, and
the seven exception classes. Hand-written response data classes
(AgentDatum, AgentJobResult, etc.) are no longer re-exported.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…igration cleanup)

The hand-written API surface now delegates entirely to the generated
raw client. Drop the dead HTTP layer and response model files:

- src/roe/utils/http_client.py — RoeHTTPClient class deleted; the
  shared httpx.Client is constructed directly in RoeClient.__init__
  with RoeRetryTransport. Internal attribute is now _httpx_client.
- src/roe/models/{agent,policy,responses,user}.py — all hand-written
  response data classes removed. Generated equivalents live under
  roe._generated.models.

src/roe/models/__init__.py and src/roe/utils/__init__.py trimmed to
match. The Job/JobBatch/JobStatus stateful helpers and FileUpload
remain. Public exports in roe.__init__ are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bump pyproject version 0.2.10 -> 1.0.0. Add CHANGELOG.md with the
full breaking-change list (removed exports, PUT->PATCH on update
endpoints, paginated PolicyVersions.list, Job.wait return-type
change). Add a v1.0.0 callout to README.md pointing readers at the
changelog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`result.succeeded` / `failed` / `cancelled` / `get_references()` were
properties / methods on the hand-written `AgentJobResult` and don't
exist on the generated `AgentJobResultResponse` returned by the new
`Job.wait()`. The terminal `status` and `error_message` are stuffed into
`additional_properties` and accessed via subscript (`result["status"]`,
`result["error_message"]`). README updated to use that pattern + the
existing `JobStatus` enum.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- inputs.py: read file-path inputs eagerly into bytes (no leaked
  file descriptors); skip None values rather than coercing to ""
- transport.py: include PATCH in idempotent retry set (the SDK only
  uses PATCH for partial-field updates the backend treats idempotently)
- agents.py: raise RoeAPIException on unexpected run_many response
  shapes instead of silently using dict keys as job IDs
- job.py: narrow except in Job.wait() from Exception to RoeAPIException

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- RoeRetryTransport: retry 5xx/408/429 and httpx.TransportError; POST retries; multipart bypass via x-roe-skip-retry\n- Simplify generated_request; batch status/results use transport retries; multipart sets bypass header\n- README: changelog tone; ROE_API_KEY env fix; retry section matches transport; link CHANGELOG.md\n- Regenerated OpenAPI client (nullable creators, drift)

Made-with: Cursor
Keep raw async calls configured through the generated client and avoid advertising the private generated package from the public export list.

Made-with: Cursor
Wrap GET /v1/users/current_user/ in roe.api.users.UsersAPI.me() and
expose it as RoeClient.users. Returns the parsed UserInfo model and
goes through translate_response() so non-2xx responses surface as the
typed RoeAPIException family at the wrapper boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tus cache

Two related bugs in JobBatch:

1. wait() used the raw result_item.id (annotated str in the generated model
   but defensively still coerced to str() in _extract_id elsewhere). If a
   subclass or wire variation ever returned a UUID, _completed_jobs would
   key on UUID while _job_ids holds strings, silently dropping all results.
   Match _extract_id's coercion.

2. retrieve_status() reconstructed AgentJobStatus from cached terminal
   entries with a hard-coded timestamp=0 because _job_statuses only stored
   {status, error_message}. Persist the timestamp when populating the cache
   in both wait() and retrieve_status() paths and read it back when
   reconstructing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
run() and run_version() now reject non-string responses with a clear
RoeAPIException, matching the existing run_many shape check. Without this
guard, a backend response like {"job_id": "..."} would only surface as a
cryptic TypeError inside Job.retrieve_status's UUID() call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three example scripts still referenced shapes from the v0.2.x hand-written
client and would crash or mislead under v1.0.0:

- delete_job_data.py: response field is `blob_sanitized` (the deleted blob
  contains outputs + steps + logs + trace, not just outputs); `outputs_sanitized`
  no longer exists.
- manage_policies.py: `policies.versions.list(...)` now paginates, so use
  `versions.count` / `versions.results` instead of `len()` / direct iteration.
- update_agent.py: `agents.duplicate(...)` returns the new agent's first
  `AgentVersion`, not the `BaseAgent`. The backend has always returned this
  shape (see AgentDuplicateView.serializer_class = AgentVersionSerializer);
  the v0.2.x facade was mistyped. Surface both the agent ID
  (`new_version.base_agent.id`) and the version ID for clarity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each method in PoliciesAPI / AgentsAPI / nested *Versions/*Jobs APIs
wraps incoming IDs in `UUID(x)` to normalize. `UUID.__init__` calls
`.replace()` on its hex argument, so passing an existing `uuid.UUID`
instance (instead of `str`) raised an opaque
`AttributeError: 'UUID' object has no attribute 'replace'`.

This regressed v0.2.x behavior: response objects expose `.id: UUID`,
so the natural idiom

    p = client.policies.create(...)
    client.policies.retrieve(p.id)

worked on 0.2.9 and broke on the v1.0.0 migration. Wrap each call
site in `UUID(str(x))` so both `str` and `UUID` inputs are accepted.

Verified by extending the edge-test harness:
  PASS inputs.uuid_object_accepted — both str and UUID accepted

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat!: migrate Python SDK to generated raw client (v1.0.0)
@jadenfix
jadenfix merged commit 2a3af06 into main May 5, 2026
2 checks passed
@greptile-apps

greptile-apps Bot commented May 5, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR is a major version bump (0.2.10 → 1.0.0) that replaces the hand-written HTTP layer and Pydantic response models with a thin facade over an OpenAPI-generated client (roe._generated). The custom RoeHTTPClient is deleted, retry logic moves into a new RoeRetryTransport, and all API classes now delegate to generated endpoint modules while preserving the same public method signatures.

  • RoeHTTPClient removed: replaced by a shared httpx.Client wrapped in RoeRetryTransport; retry policy broadened to cover 408/429/5xx and transport errors across all methods, with multipart run calls opting out via x-roe-skip-retry.
  • Response models deleted: Agent, Policy, UserInfo, and related Pydantic classes are gone; callers should use roe._generated.models; only Job, JobBatch, JobStatus, and FileUpload are kept in roe.models.
  • New UsersAPI: client.users.me() exposes the current-user endpoint; covered by new unit tests.

Confidence Score: 3/5

Not safe to merge without verifying the generated AgentJobStatus model shape — batch job polling may silently hang, and failed-job result fetches may raise unexpectedly.

The transport, exceptions, and API facade layers are well-structured. However, Job.wait() now only catches RoeAPIException when fetching a failed job's result — transport errors after retry exhaustion will propagate as uncaught exceptions instead of returning the documented placeholder. More critically, JobBatch.wait() relies on _extract_id finding a job-ID attribute on the generated AgentJobStatus model, but that model may not carry that field (the old hand-written single-job status also lacked it), which would cause every JobBatch.wait() call to spin until timeout.

src/roe/models/job.py warrants the most attention — both the single-job and batch wait loops have potential failure modes that need verification against the actual generated AgentJobStatus model shape.

Important Files Changed

Filename Overview
src/roe/models/job.py Core Job/JobBatch helpers migrated to generated models; two logic issues: exception handler narrowed too far for failed-job result fetches, and batch job-ID extraction may always return None (hanging wait loops).
src/roe/api/agents.py Agents API refactored as a thin facade over generated client; multipart run calls use call_dynamic correctly; retrieve() uses manual from_dict while other methods use resp.parsed — inconsistent but not a bug.
src/roe/api/policies.py Policies API migrated cleanly; PolicyVersionsAPI now owns its own config/raw reference rather than delegating to a parent; _normalize_policy_version_wire zero-UUID patch is a plausible workaround for nullable field.
src/roe/utils/transport.py New RoeRetryTransport correctly gates on x-roe-skip-retry header, retries 5xx/408/429 and TransportErrors with exponential backoff capped at 10 s.
src/roe/utils/_dynamic_call.py Multipart-bypass helper strips generated body and injects correct data=/files= kwargs; sets x-roe-skip-retry to prevent transport-layer replay of non-idempotent uploads.
src/roe/utils/inputs.py Extracted multipart-builder from http_client.py; adds MIME-type detection for file paths; silently omits None values instead of sending empty strings (behaviour change from old code).
src/roe/exceptions.py Added translate_response() that handles both httpx.Response and generated Response objects; correctly extracts DRF error shapes including list-style validation errors.
src/roe/client.py Client wiring simplified: shared httpx.Client with RoeRetryTransport, raw AuthenticatedClient reuses it; new UsersAPI exposed; http_client property removed (breaking change documented in CHANGELOG).
src/roe/api/users.py New UsersAPI.me() works correctly; manually parses JSON body as a fallback since resp.parsed may be None when the generated client doesn't populate it; covered by unit tests.
src/roe/utils/generated_request.py request_raw/request_json helpers force JSON content-type to fix openapi-python-client's multipart-first code-generation bug; straightforward and correct.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant RoeClient
    participant AgentsAPI
    participant call_dynamic
    participant RoeRetryTransport
    participant Server

    Caller->>RoeClient: client.agents.run(agent_id, **inputs)
    RoeClient->>AgentsAPI: run(agent_id, inputs, metadata)
    AgentsAPI->>call_dynamic: ep._get_kwargs() + inject multipart
    call_dynamic->>RoeRetryTransport: httpx.Client.request(**kwargs)
    Note over call_dynamic,RoeRetryTransport: x-roe-skip-retry set — no retry
    RoeRetryTransport->>Server: POST /v1/agents/run/{id}/async/
    Server-->>RoeRetryTransport: 200 job_id
    RoeRetryTransport-->>call_dynamic: httpx.Response
    call_dynamic-->>AgentsAPI: httpx.Response
    AgentsAPI-->>Caller: Job(job_id)

    Caller->>Caller: job.wait()
    loop until terminal status
        Caller->>AgentsAPI: jobs.retrieve_status(job_id)
        AgentsAPI->>RoeRetryTransport: GET /v1/agents/jobs/{id}/status/
        Note over AgentsAPI,RoeRetryTransport: 5xx/408/429 retried with backoff
        RoeRetryTransport-->>AgentsAPI: AgentJobStatus
    end
    AgentsAPI->>AgentsAPI: jobs.retrieve_result(job_id)
    AgentsAPI-->>Caller: AgentJobResultResponse
Loading

Comments Outside Diff (1)

  1. src/roe/models/job.py, line 375-379 (link)

    P1 Narrowed exception catch may swallow failures for terminal jobs

    The old code used except Exception to ensure that any error fetching the result of a failed/cancelled job (network issues, parse failures, unexpected status after all retries, etc.) was silently swallowed and replaced with a placeholder _empty_result. The new code catches only RoeAPIException, so if retrieve_result() raises an httpx.TransportError or another non-RoeAPIException for a terminal failed job (e.g., after the retry budget is exhausted at the transport layer), the exception propagates to the caller instead of returning the synthesized result. The CHANGELOG documents the placeholder behaviour as an intentional guarantee, so this regression breaks that contract.

    Fix in Claude Code

Fix All in Claude Code

Reviews (1): Last reviewed commit: "Merge pull request #27 from roe-ai/jaden..." | Re-trigger Greptile

Comment thread src/roe/models/job.py
Comment on lines 301 to +313

return status_map

@staticmethod
def _extract_id(status_item: Any) -> str | None:
for key in ("id", "agent_job_id", "job_id"):
value = getattr(status_item, key, None)
if value is None:
value = (
status_item.additional_properties.get(key)
if hasattr(status_item, "additional_properties")
else None
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 JobBatch.wait() may hang if the generated AgentJobStatus has no job-ID field

_extract_id probes id, agent_job_id, and job_id on each item returned by retrieve_status_many. The old AgentJobStatusBatch model explicitly had an id: str field carrying the job UUID. The new implementation casts the batch-endpoint response to list[AgentJobStatus] (roe._generated.models.agent_job_status), the same type used for single-job status — which, like the old hand-written AgentJobStatus, likely has only status, timestamp, and error_message.

If none of the three probed attribute names resolves to a non-None value, _extract_id returns None and the continue discards every status item. completed_in_this_batch stays empty every cycle, and the batch only terminates when it times out.

Fix in Claude Code

Comment thread src/roe/utils/inputs.py
Comment on lines +59 to +61
else:
if value is not None:
form_data[key] = str(value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent None-value omission is a behaviour change from the previous implementation

The old _process_inputs in RoeHTTPClient sent an empty string ("") when a non-file input was None. The new code omits the field entirely. Agents that declare optional inputs and rely on receiving an empty string for unset fields will no longer get that signal; instead, the field is absent from the multipart form, which the server may treat as a validation error or as a distinct "field not provided" semantic.

Suggested change
else:
if value is not None:
form_data[key] = str(value)
else:
form_data[key] = str(value) if value is not None else ""

Fix in Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants