Repository navigation
Jaden/sdk panel - #28
Conversation
…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)
Greptile SummaryThis 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 (
Confidence Score: 3/5Not 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
Sequence DiagramsequenceDiagram
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
|
|
|
||
| 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 | ||
| ) |
There was a problem hiding this comment.
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.
| else: | ||
| if value is not None: | ||
| form_data[key] = str(value) |
There was a problem hiding this comment.
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.
| else: | |
| if value is not None: | |
| form_data[key] = str(value) | |
| else: | |
| form_data[key] = str(value) if value is not None else "" |
No description provided.