Skip to content

SYN-1365: SDK: Add LeverHandle + SensorFrame client APIs#191

Open
JoshuaPurtell wants to merge 1 commit intomainfrom
agent/syn-1365
Open

SYN-1365: SDK: Add LeverHandle + SensorFrame client APIs#191
JoshuaPurtell wants to merge 1 commit intomainfrom
agent/syn-1365

Conversation

@JoshuaPurtell
Copy link
Collaborator

@JoshuaPurtell JoshuaPurtell commented Feb 20, 2026

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 20, 2026

Greptile Summary

This PR adds client SDK support for lever and sensor frame management in the optimization system. The implementation adds four new methods to LearningClient: create_or_update_lever, resolve_lever, emit_sensor_frame, and list_sensor_frames.

  • Added LeverHandle and SensorFrame dataclasses with proper serialization/deserialization
  • Implemented helper functions _normalize_scope_payload and _payload_to_dict for flexible input handling
  • All methods include proper error handling with HTTPError for invalid responses
  • list_sensor_frames includes defensive parsing to handle multiple response formats (raw list or wrapped object)
  • Comprehensive test coverage with mocked HTTP clients validates all new functionality

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The implementation follows consistent patterns, includes comprehensive error handling, provides defensive parsing for API responses, and has thorough unit test coverage validating all new functionality
  • No files require special attention

Important Files Changed

Filename Overview
synth_ai/sdk/optimization/internal/learning/client.py Added four new client methods for lever/sensor management with proper error handling and response parsing
synth_ai/sdk/optimization/models.py Added LeverHandle and SensorFrame dataclasses with robust from_dict/to_dict serialization logic
tests/optimization/test_learning_client_levers_sensors.py Comprehensive unit tests covering all four new API methods with proper mocking

Sequence Diagram

sequenceDiagram
    participant Client as LearningClient
    participant API as Backend API
    participant Storage as Data Store
    
    Note over Client,Storage: Lever Operations
    Client->>API: POST /api/v1/optimizers/{id}/levers
    API->>Storage: Create/Update Lever
    Storage-->>API: LeverHandle
    API-->>Client: LeverHandle (with version)
    
    Client->>API: GET /api/v1/optimizers/{id}/levers?lever_id&scope
    API->>Storage: Resolve Lever Snapshot
    Storage-->>API: LeverHandle
    API-->>Client: LeverHandle | None
    
    Note over Client,Storage: Sensor Operations
    Client->>API: POST /api/v1/optimizers/{id}/sensors
    API->>Storage: Store SensorFrame
    Storage-->>API: SensorFrame (with frame_id)
    API-->>Client: SensorFrame
    
    Client->>API: GET /api/v1/optimizers/{id}/sensors?scope&limit
    API->>Storage: Query SensorFrames
    Storage-->>API: List[SensorFrame]
    API-->>Client: List[SensorFrame]
Loading

Last reviewed commit: f0840a3

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

3 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

kind = LeverKind(str(kind_raw)) if kind_raw is not None else LeverKind.CUSTOM
except ValueError:
kind = LeverKind.CUSTOM
version_raw = raw.get("version") or raw.get("lever_version")
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 or operator treats version 0 as falsy, skipping valid version values

In LeverHandle.from_dict, the expression raw.get("version") or raw.get("lever_version") uses Python's or operator to fall back between two keys. However, or treats 0 as falsy, so a legitimate version value of 0 from the "version" key would be skipped in favor of "lever_version".

Root Cause and Impact

When the API returns {"version": 0, "lever_version": 3}, the expression evaluates as:

  • raw.get("version")0 (falsy)
  • 0 or raw.get("lever_version")3
  • version_raw = 3 (wrong — should be 0)

The correct approach is to use explicit is not None checks:

version_raw = raw.get("version")
if version_raw is None:
    version_raw = raw.get("lever_version")

Impact: Any lever with version 0 will have its version incorrectly read from the lever_version field (if present), or silently default to 0 only by coincidence if lever_version is also absent.

Suggested change
version_raw = raw.get("version") or raw.get("lever_version")
version_raw = raw.get("version")
if version_raw is None:
version_raw = raw.get("lever_version")
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +273 to +279
candidates = (
js.get("items")
or js.get("sensor_frames")
or js.get("frames")
or js.get("data")
or []
)
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 or-chain in list_sensor_frames treats empty list [] as falsy, falling through to wrong response key

In list_sensor_frames, the or-chain js.get("items") or js.get("sensor_frames") or js.get("frames") or js.get("data") or [] uses Python's or operator to select the first truthy value. An empty list [] is falsy in Python, so if the correct response key (e.g. "items") contains an empty list, it will be skipped in favor of a later key.

Root Cause and Impact

Consider an API response like {"items": [], "data": [{...}]}. The intended key is "items" (empty — no frames), but:

  • js.get("items")[] (falsy)
  • [] or js.get("sensor_frames")None (falsy)
  • None or js.get("frames")None (falsy)
  • None or js.get("data")[{...}] (truthy)
  • candidates = [{...}] (wrong — should be [])

This means the method would return phantom sensor frames from an unrelated key when the correct key has an empty list.

Impact: Callers could receive incorrect/unexpected sensor frames when the actual result set is empty but the response dict contains other list-valued keys.

Suggested change
candidates = (
js.get("items")
or js.get("sensor_frames")
or js.get("frames")
or js.get("data")
or []
)
candidates = None
for _key in ("items", "sensor_frames", "frames", "data"):
_val = js.get(_key)
if _val is not None:
candidates = _val
break
if candidates is None:
candidates = []
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

1 participant