Skip to content

Commit f74ca30

Browse files
committed
feat: add Execution / Worker / Agent / Message Pydantic models (additive)
Closes the remaining `model_drift` items in cueapi-python #24's parity manifest. Adds 4 new model files + 7 new exported classes covering the response shapes that resource methods currently return as raw dicts. **Additive only** — resource methods still return raw `dict` (no breaking change to return types). Callers opt into typed accessors via `Model.model_validate(dict)`. Promoting resource methods to return typed objects is a separate breaking-change PR and would warrant a major version bump. New files: - `cueapi/models/execution.py` — `Execution` + `ExecutionList` + `OutcomeDetail` (typed outcome inline). Covers all 13 fields the manifest flagged as missing on Execution: `payload` (PR #589), `outcome`, `outcome_state`, `triggered_by`, evidence_*, claimed_by_*, chain_*, last_heartbeat_at. - `cueapi/models/worker.py` — `Worker` + `WorkerList`. Captures `heartbeat_status` (online / stale / dead), `seconds_since_heartbeat`, `handlers` list. - `cueapi/models/agent.py` — `Agent` + `AgentList` (Phase 12.1.5 messaging primitive identity surface). Mirrors server's `AgentResponse`. `webhook_secret` field captures the one-time-on-create / one-time-on-regenerate contract. - `cueapi/models/message.py` — `Message` + `MessageList` + `FromAgentRef` (inline sender reference) + `StateTransitionResponse` (read / ack response shape). Notable design choices: - All new models use `extra="allow"` so server can grow response shapes without breaking SDK callers. Same pattern already in use on `AlertConfig` + `VerificationConfig` from #29. - `Message.from_agent` aliases the server's `from` field via `Field(alias="from")` — `from` is a reserved keyword in Python so the SDK exposes it as `from_agent` while still parsing server's `from` on the wire. `populate_by_name=True` lets callers use either name on construction. - All 11 new classes exported from `cueapi.__init__` for ergonomic access (`from cueapi import Agent, Message, ...`). Tests: 17 new (93 → 110 total). Coverage: - Minimal vs full responses parse cleanly - Forward-compat: unknown fields land in `model_extra` instead of raising - `from` → `from_agent` alias roundtrip - `webhook_secret` one-time-view shape on Agent - All new classes are exported from top-level + are BaseModel subclasses 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent aae27eb commit f74ca30

6 files changed

Lines changed: 554 additions & 0 deletions

File tree

cueapi/__init__.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@
1010
InvalidScheduleError,
1111
RateLimitError,
1212
)
13+
from cueapi.models.agent import Agent, AgentList
14+
from cueapi.models.cue import Cue, CueList
15+
from cueapi.models.execution import Execution, ExecutionList, OutcomeDetail
16+
from cueapi.models.message import (
17+
FromAgentRef,
18+
Message,
19+
MessageList,
20+
StateTransitionResponse,
21+
)
22+
from cueapi.models.worker import Worker, WorkerList
1323
from cueapi.payload import CuePayload
1424
from cueapi.resources.agents import AgentsResource
1525
from cueapi.resources.executions import ExecutionsResource
@@ -21,12 +31,25 @@
2131
__version__ = "0.2.0"
2232

2333
__all__ = [
34+
"Agent",
35+
"AgentList",
2436
"AgentsResource",
37+
"Cue",
2538
"CueAPI",
39+
"CueList",
2640
"CuePayload",
41+
"Execution",
42+
"ExecutionList",
2743
"ExecutionsResource",
44+
"FromAgentRef",
45+
"Message",
46+
"MessageList",
2847
"MessagesResource",
48+
"OutcomeDetail",
49+
"StateTransitionResponse",
2950
"UsageResource",
51+
"Worker",
52+
"WorkerList",
3053
"WorkersResource",
3154
"verify_webhook",
3255
"CueAPIError",

cueapi/models/agent.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Agent Pydantic model — typed accessor for messaging-primitive agent responses.
2+
3+
Closes the Agent portion of cueapi-python #24's `model_drift` manifest.
4+
``AgentsResource`` methods currently return raw dicts; callers can opt
5+
into typed accessors via ``Agent.model_validate(client.agents.get(ref))``
6+
or ``AgentList.model_validate(client.agents.list())``.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from datetime import datetime
12+
from typing import Any, Dict, List, Optional
13+
14+
from pydantic import BaseModel
15+
16+
17+
class Agent(BaseModel):
18+
"""Typed accessor for a messaging-primitive agent (Phase 12.1.5).
19+
20+
``webhook_secret`` is populated only on the response from
21+
``client.agents.create()`` (when ``webhook_url`` was supplied) and
22+
from ``client.agents.webhook_secret_regenerate()``. Subsequent reads
23+
omit the secret.
24+
"""
25+
26+
id: str
27+
user_id: Optional[str] = None
28+
api_key_id: Optional[str] = None
29+
slug: str
30+
display_name: str
31+
webhook_url: Optional[str] = None
32+
# One-time on create + on regenerate; None on subsequent reads.
33+
webhook_secret: Optional[str] = None
34+
metadata: Dict[str, Any] = {}
35+
status: Optional[str] = None # online / offline / away
36+
deleted_at: Optional[datetime] = None
37+
created_at: Optional[datetime] = None
38+
updated_at: Optional[datetime] = None
39+
model_config = {"extra": "allow"}
40+
41+
42+
class AgentList(BaseModel):
43+
"""Typed accessor for ``client.agents.list()`` responses."""
44+
45+
agents: List[Agent]
46+
total: int
47+
limit: int
48+
offset: int
49+
model_config = {"extra": "allow"}

cueapi/models/execution.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Execution Pydantic model — typed accessor for execution dict responses.
2+
3+
Closes the Execution portion of cueapi-python #24's `model_drift` manifest.
4+
``ExecutionsResource`` methods (`list`, `get`, `replay`) currently return
5+
raw dicts; callers can opt into typed accessors via
6+
``Execution.model_validate(client.executions.get(...))``. Returning the
7+
typed object directly from resource methods is a separate breaking-change
8+
PR (would bump major version).
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from datetime import datetime
14+
from typing import Any, Dict, List, Optional
15+
16+
from pydantic import BaseModel, Field
17+
18+
19+
class OutcomeDetail(BaseModel):
20+
"""Outcome reported by the worker / handler. Set when the execution
21+
has reached a terminal state and the handler has reported via
22+
``POST /v1/executions/{id}/outcome``."""
23+
24+
success: bool
25+
result: Optional[str] = None
26+
error: Optional[str] = None
27+
metadata: Optional[Dict[str, Any]] = None
28+
recorded_at: Optional[datetime] = None
29+
# Evidence fields (Phase 18 Gap 11 — outcome verification).
30+
external_id: Optional[str] = None
31+
result_url: Optional[str] = None
32+
result_ref: Optional[str] = None
33+
result_type: Optional[str] = None
34+
summary: Optional[str] = None
35+
artifacts: Optional[List[Any]] = None
36+
validation_state: Optional[str] = None
37+
assertions: Optional[Dict[str, Any]] = None
38+
model_config = {"extra": "allow"}
39+
40+
41+
class Execution(BaseModel):
42+
"""Typed accessor for an execution response.
43+
44+
Mirrors the server's ``ExecutionResponse`` schema. Use as
45+
``Execution.model_validate(client.executions.get(exec_id))`` or
46+
``Execution.model_validate(item)`` over each item in
47+
``client.executions.list()['executions']``.
48+
"""
49+
50+
id: str
51+
cue_id: str
52+
scheduled_for: datetime
53+
status: str
54+
http_status: Optional[int] = None
55+
response_body: Optional[str] = None
56+
attempts: Optional[int] = None
57+
next_retry: Optional[datetime] = None
58+
error_message: Optional[str] = None
59+
started_at: Optional[datetime] = None
60+
delivered_at: Optional[datetime] = None
61+
last_attempt_at: Optional[datetime] = None
62+
claimed_by_worker: Optional[str] = None
63+
claimed_at: Optional[datetime] = None
64+
last_heartbeat_at: Optional[datetime] = None
65+
# Hosted PR #589: effective payload the handler/webhook saw at delivery.
66+
# `payload_override` if set on the execution, else parent cue's payload.
67+
payload: Optional[Dict[str, Any]] = None
68+
# Outcome — populated only after handler reports.
69+
outcome: Optional[OutcomeDetail] = None
70+
outcome_state: Optional[str] = Field(
71+
default=None,
72+
description=(
73+
"Phase 18 Gap 11: enum tracking outcome verification state. "
74+
"Values: reported_success / reported_failure / verified_success / "
75+
"verification_pending / verification_failed / unknown."
76+
),
77+
)
78+
triggered_by: Optional[str] = Field(
79+
default=None,
80+
description="scheduled / manual_fire / chain / replay",
81+
)
82+
# Chain support (Gap 1 — on_success_fire native chaining).
83+
chain_parent_id: Optional[str] = None
84+
chain_depth: Optional[int] = None
85+
created_at: Optional[datetime] = None
86+
updated_at: Optional[datetime] = None
87+
# Forward-compat: server may grow the response over time without the
88+
# SDK breaking. Same pattern as AlertConfig / VerificationConfig in
89+
# the Cue model.
90+
model_config = {"extra": "allow"}
91+
92+
93+
class ExecutionList(BaseModel):
94+
"""Typed accessor for ``client.executions.list()`` responses."""
95+
96+
executions: List[Execution]
97+
total: int
98+
limit: int
99+
offset: int
100+
model_config = {"extra": "allow"}

cueapi/models/message.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Message Pydantic model — typed accessor for messaging-primitive message responses.
2+
3+
Closes the Message portion of cueapi-python #24's `model_drift` manifest.
4+
``MessagesResource`` methods currently return raw dicts; callers can opt
5+
into typed accessors via ``Message.model_validate(client.messages.get(id))``
6+
or ``MessageList.model_validate(client.agents.inbox(ref))``.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from datetime import datetime
12+
from typing import Any, Dict, List, Optional
13+
14+
from pydantic import BaseModel, Field
15+
16+
17+
class FromAgentRef(BaseModel):
18+
"""Inline agent reference rendered on incoming-message responses."""
19+
20+
agent_id: Optional[str] = None
21+
slug: Optional[str] = None
22+
model_config = {"extra": "allow"}
23+
24+
25+
class StateTransitionResponse(BaseModel):
26+
"""Response shape for ``mark_read`` and ``ack`` endpoints."""
27+
28+
delivery_state: str
29+
read_at: Optional[datetime] = None
30+
acked_at: Optional[datetime] = None
31+
model_config = {"extra": "allow"}
32+
33+
34+
class Message(BaseModel):
35+
"""Typed accessor for a messaging-primitive message (Phase 12.1.5).
36+
37+
Mirrors the server's ``MessageResponse`` schema. Both inbox-fetched
38+
and sent-fetched messages use this shape; the ``from`` / ``to`` slots
39+
capture sender / recipient regardless of perspective.
40+
"""
41+
42+
id: str
43+
user_id: Optional[str] = None
44+
# Sender — populated on inbox responses; may be self on sent responses.
45+
# Pydantic treats `from` as a reserved keyword, but the server uses it
46+
# in the response. Use alias for clean access via .from_agent.
47+
from_agent: Optional[FromAgentRef] = Field(default=None, alias="from")
48+
to: Optional[str] = None
49+
body: Optional[str] = None
50+
subject: Optional[str] = None
51+
thread_id: Optional[str] = None
52+
reply_to: Optional[str] = None
53+
reply_to_agent: Optional[str] = None
54+
priority: Optional[int] = None
55+
expects_reply: Optional[bool] = None
56+
delivery_state: Optional[str] = None
57+
metadata: Optional[Dict[str, Any]] = None
58+
expires_at: Optional[datetime] = None
59+
queued_at: Optional[datetime] = None
60+
delivered_at: Optional[datetime] = None
61+
read_at: Optional[datetime] = None
62+
acked_at: Optional[datetime] = None
63+
failed_at: Optional[datetime] = None
64+
created_at: Optional[datetime] = None
65+
updated_at: Optional[datetime] = None
66+
model_config = {"extra": "allow", "populate_by_name": True}
67+
68+
69+
class MessageList(BaseModel):
70+
"""Typed accessor for inbox / sent responses (lists of messages)."""
71+
72+
messages: List[Message]
73+
total: Optional[int] = None
74+
limit: Optional[int] = None
75+
offset: Optional[int] = None
76+
model_config = {"extra": "allow"}

cueapi/models/worker.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Worker Pydantic model — typed accessor for worker dict responses.
2+
3+
Closes the Worker portion of cueapi-python #24's `model_drift` manifest.
4+
``WorkersResource.list()`` currently returns a raw dict; callers can opt
5+
into typed accessors via ``Worker.model_validate(item)`` over each item
6+
in ``client.workers.list()['workers']``.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from datetime import datetime
12+
from typing import Any, List, Optional
13+
14+
from pydantic import BaseModel
15+
16+
17+
class Worker(BaseModel):
18+
"""Typed accessor for a registered worker.
19+
20+
Mirrors the server's ``Worker`` model. ``heartbeat_status`` is
21+
computed server-side from ``seconds_since_heartbeat``:
22+
- ``online``: <60s since last heartbeat
23+
- ``stale``: 60-360s since last heartbeat
24+
- ``dead``: >360s since last heartbeat
25+
"""
26+
27+
id: Optional[str] = None
28+
user_id: Optional[str] = None
29+
worker_id: str
30+
handlers: Optional[List[str]] = None
31+
last_heartbeat: Optional[datetime] = None
32+
heartbeat_status: Optional[str] = None
33+
seconds_since_heartbeat: Optional[int] = None
34+
api_key_id: Optional[str] = None
35+
created_at: Optional[datetime] = None
36+
model_config = {"extra": "allow"}
37+
38+
39+
class WorkerList(BaseModel):
40+
"""Typed accessor for ``client.workers.list()`` responses."""
41+
42+
workers: List[Worker]
43+
total: Optional[int] = None
44+
model_config = {"extra": "allow"}

0 commit comments

Comments
 (0)