agentdraft 0.1.1__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentdraft-0.1.1 → agentdraft-0.1.2}/PKG-INFO +24 -6
- {agentdraft-0.1.1 → agentdraft-0.1.2}/README.md +20 -4
- {agentdraft-0.1.1 → agentdraft-0.1.2}/agentdraft/__init__.py +14 -3
- {agentdraft-0.1.1 → agentdraft-0.1.2}/agentdraft/client.py +117 -1
- {agentdraft-0.1.1 → agentdraft-0.1.2}/agentdraft/errors.py +43 -0
- {agentdraft-0.1.1 → agentdraft-0.1.2}/agentdraft/models.py +24 -0
- {agentdraft-0.1.1 → agentdraft-0.1.2}/agentdraft.egg-info/PKG-INFO +24 -6
- {agentdraft-0.1.1 → agentdraft-0.1.2}/pyproject.toml +4 -2
- {agentdraft-0.1.1 → agentdraft-0.1.2}/LICENSE +0 -0
- {agentdraft-0.1.1 → agentdraft-0.1.2}/agentdraft/py.typed +0 -0
- {agentdraft-0.1.1 → agentdraft-0.1.2}/agentdraft.egg-info/SOURCES.txt +0 -0
- {agentdraft-0.1.1 → agentdraft-0.1.2}/agentdraft.egg-info/dependency_links.txt +0 -0
- {agentdraft-0.1.1 → agentdraft-0.1.2}/agentdraft.egg-info/requires.txt +0 -0
- {agentdraft-0.1.1 → agentdraft-0.1.2}/agentdraft.egg-info/top_level.txt +0 -0
- {agentdraft-0.1.1 → agentdraft-0.1.2}/setup.cfg +0 -0
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentdraft
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: AgentDraft SDK — ops API for AI agents with inbox, conflict-free calendar booking, and audit trail.
|
|
5
5
|
Author-email: AgentDraft Labs <hello@agentdraft.io>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://agentdraft.io
|
|
8
8
|
Project-URL: Documentation, https://agentdraft.io/docs
|
|
9
9
|
Project-URL: Specification, https://agentdraft.io/spec
|
|
10
10
|
Project-URL: Changelog, https://agentdraft.io/changelog
|
|
11
|
+
Project-URL: Repository, https://github.com/ryabinski-labs/agentdraft
|
|
12
|
+
Project-URL: Issues, https://github.com/ryabinski-labs/agentdraft/issues
|
|
11
13
|
Keywords: agentdraft,ai-agents,agents,calendar,scheduling,booking,coordination,langchain,crewai,autogen,mcp
|
|
12
14
|
Classifier: Development Status :: 4 - Beta
|
|
13
15
|
Classifier: Intended Audience :: Developers
|
|
@@ -54,6 +56,14 @@ pip install agentdraft
|
|
|
54
56
|
|
|
55
57
|
Requires Python 3.9+.
|
|
56
58
|
|
|
59
|
+
## Upgrade
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install -U agentdraft
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Release notes: <https://agentdraft.io/changelog>
|
|
66
|
+
|
|
57
67
|
## Quickstart
|
|
58
68
|
|
|
59
69
|
```python
|
|
@@ -69,13 +79,21 @@ try:
|
|
|
69
79
|
booking = client.bookings.commit(
|
|
70
80
|
start=start, end=end,
|
|
71
81
|
idempotency_key="ik_call_42",
|
|
72
|
-
|
|
82
|
+
title="Discovery call",
|
|
83
|
+
invitee={"name": "Ada Lovelace", "email": "ada@example.com"},
|
|
73
84
|
)
|
|
74
85
|
print("booked:", booking.booking_id)
|
|
75
86
|
except Conflict as e:
|
|
76
87
|
print(f"outranked by {e.winning_agent_id} (rank {e.winning_agent_priority})")
|
|
77
88
|
```
|
|
78
89
|
|
|
90
|
+
`title` and `invitee` (`{name, email, notes}`, all optional) are first-class
|
|
91
|
+
fields: they're persisted on the booking, echoed back on the returned
|
|
92
|
+
`Booking`, and shown to the calendar owner in the dashboard. A top-level
|
|
93
|
+
`title` supersedes the legacy `metadata={"title": ...}`. `client.agents.me()`
|
|
94
|
+
also returns the owner's `timezone` so a client can render slots in their
|
|
95
|
+
zone.
|
|
96
|
+
|
|
79
97
|
A losing agent gets a typed `Conflict` exception, not a timeout — it
|
|
80
98
|
knows who won, by what priority, and where the audit row lives, so
|
|
81
99
|
fallback behavior (propose an alternate, escalate, defer) is a clean
|
|
@@ -104,7 +122,7 @@ configurable bump window.
|
|
|
104
122
|
The repo ships a multi-agent race demo. With the local stack up:
|
|
105
123
|
|
|
106
124
|
```bash
|
|
107
|
-
git clone https://github.com/
|
|
125
|
+
git clone https://github.com/ryabinski-labs/agentdraft && cd agentdraft
|
|
108
126
|
docker compose up -d dynamodb
|
|
109
127
|
pip install -e ".[dev]" -e sdks/python
|
|
110
128
|
uvicorn app.main:app --port 8080 &
|
|
@@ -177,8 +195,8 @@ wrappers is on the roadmap.
|
|
|
177
195
|
- Protocol spec: <https://agentdraft.io/spec>
|
|
178
196
|
- API docs: <https://agentdraft.io/docs>
|
|
179
197
|
- Changelog: <https://agentdraft.io/changelog>
|
|
180
|
-
- Source & issues: <https://github.com/
|
|
181
|
-
- TypeScript SDK: [
|
|
198
|
+
- Source & issues: <https://github.com/ryabinski-labs/agentdraft>
|
|
199
|
+
- TypeScript SDK: [`agentdraft`](https://www.npmjs.com/package/agentdraft)
|
|
182
200
|
|
|
183
201
|
## Security
|
|
184
202
|
|
|
@@ -25,6 +25,14 @@ pip install agentdraft
|
|
|
25
25
|
|
|
26
26
|
Requires Python 3.9+.
|
|
27
27
|
|
|
28
|
+
## Upgrade
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install -U agentdraft
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Release notes: <https://agentdraft.io/changelog>
|
|
35
|
+
|
|
28
36
|
## Quickstart
|
|
29
37
|
|
|
30
38
|
```python
|
|
@@ -40,13 +48,21 @@ try:
|
|
|
40
48
|
booking = client.bookings.commit(
|
|
41
49
|
start=start, end=end,
|
|
42
50
|
idempotency_key="ik_call_42",
|
|
43
|
-
|
|
51
|
+
title="Discovery call",
|
|
52
|
+
invitee={"name": "Ada Lovelace", "email": "ada@example.com"},
|
|
44
53
|
)
|
|
45
54
|
print("booked:", booking.booking_id)
|
|
46
55
|
except Conflict as e:
|
|
47
56
|
print(f"outranked by {e.winning_agent_id} (rank {e.winning_agent_priority})")
|
|
48
57
|
```
|
|
49
58
|
|
|
59
|
+
`title` and `invitee` (`{name, email, notes}`, all optional) are first-class
|
|
60
|
+
fields: they're persisted on the booking, echoed back on the returned
|
|
61
|
+
`Booking`, and shown to the calendar owner in the dashboard. A top-level
|
|
62
|
+
`title` supersedes the legacy `metadata={"title": ...}`. `client.agents.me()`
|
|
63
|
+
also returns the owner's `timezone` so a client can render slots in their
|
|
64
|
+
zone.
|
|
65
|
+
|
|
50
66
|
A losing agent gets a typed `Conflict` exception, not a timeout — it
|
|
51
67
|
knows who won, by what priority, and where the audit row lives, so
|
|
52
68
|
fallback behavior (propose an alternate, escalate, defer) is a clean
|
|
@@ -75,7 +91,7 @@ configurable bump window.
|
|
|
75
91
|
The repo ships a multi-agent race demo. With the local stack up:
|
|
76
92
|
|
|
77
93
|
```bash
|
|
78
|
-
git clone https://github.com/
|
|
94
|
+
git clone https://github.com/ryabinski-labs/agentdraft && cd agentdraft
|
|
79
95
|
docker compose up -d dynamodb
|
|
80
96
|
pip install -e ".[dev]" -e sdks/python
|
|
81
97
|
uvicorn app.main:app --port 8080 &
|
|
@@ -148,8 +164,8 @@ wrappers is on the roadmap.
|
|
|
148
164
|
- Protocol spec: <https://agentdraft.io/spec>
|
|
149
165
|
- API docs: <https://agentdraft.io/docs>
|
|
150
166
|
- Changelog: <https://agentdraft.io/changelog>
|
|
151
|
-
- Source & issues: <https://github.com/
|
|
152
|
-
- TypeScript SDK: [
|
|
167
|
+
- Source & issues: <https://github.com/ryabinski-labs/agentdraft>
|
|
168
|
+
- TypeScript SDK: [`agentdraft`](https://www.npmjs.com/package/agentdraft)
|
|
153
169
|
|
|
154
170
|
## Security
|
|
155
171
|
|
|
@@ -16,24 +16,35 @@ Quickstart::
|
|
|
16
16
|
booking = client.bookings.commit(
|
|
17
17
|
start=start, end=end,
|
|
18
18
|
idempotency_key="ik_call_42",
|
|
19
|
-
|
|
19
|
+
title="Discovery call",
|
|
20
|
+
invitee={"name": "Ada Lovelace", "email": "ada@example.com"},
|
|
20
21
|
)
|
|
21
22
|
print("booked:", booking.booking_id)
|
|
22
23
|
except Conflict as e:
|
|
23
24
|
print(f"outranked by {e.winning_agent_id} (rank {e.winning_agent_priority})")
|
|
24
25
|
"""
|
|
25
26
|
|
|
26
|
-
__version__ = "0.1.
|
|
27
|
+
__version__ = "0.1.2"
|
|
27
28
|
|
|
28
29
|
from .client import Client
|
|
29
|
-
from .errors import
|
|
30
|
+
from .errors import (
|
|
31
|
+
AgentDraftError,
|
|
32
|
+
ApprovalTimeout,
|
|
33
|
+
AuthError,
|
|
34
|
+
Conflict,
|
|
35
|
+
ConsentRequired,
|
|
36
|
+
RateLimited,
|
|
37
|
+
RuleViolation,
|
|
38
|
+
)
|
|
30
39
|
from .models import Booking, Hold, Slot
|
|
31
40
|
|
|
32
41
|
__all__ = [
|
|
33
42
|
"Client",
|
|
34
43
|
"AgentDraftError",
|
|
44
|
+
"ApprovalTimeout",
|
|
35
45
|
"AuthError",
|
|
36
46
|
"Conflict",
|
|
47
|
+
"ConsentRequired",
|
|
37
48
|
"RateLimited",
|
|
38
49
|
"RuleViolation",
|
|
39
50
|
"Booking",
|
|
@@ -3,13 +3,22 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import time
|
|
6
7
|
from datetime import datetime
|
|
7
8
|
from typing import Any, Optional
|
|
8
9
|
|
|
9
10
|
import httpx
|
|
10
11
|
|
|
11
12
|
from . import __version__
|
|
12
|
-
from .errors import
|
|
13
|
+
from .errors import (
|
|
14
|
+
AgentDraftError,
|
|
15
|
+
ApprovalTimeout,
|
|
16
|
+
AuthError,
|
|
17
|
+
Conflict,
|
|
18
|
+
ConsentRequired,
|
|
19
|
+
RateLimited,
|
|
20
|
+
RuleViolation,
|
|
21
|
+
)
|
|
13
22
|
from .models import AgentIdentity, Booking, Slot
|
|
14
23
|
|
|
15
24
|
DEFAULT_BASE_URL = "https://api.agentdraft.io"
|
|
@@ -57,6 +66,7 @@ class Client:
|
|
|
57
66
|
self.bookings = _Bookings(self)
|
|
58
67
|
self.agents = _Agents(self)
|
|
59
68
|
self.mailbox = _Mailbox(self)
|
|
69
|
+
self.approvals = _Approvals(self)
|
|
60
70
|
|
|
61
71
|
def close(self) -> None:
|
|
62
72
|
self._http.close()
|
|
@@ -92,6 +102,15 @@ class Client:
|
|
|
92
102
|
def _raise_for(self, status: int, body: dict, headers: dict) -> None:
|
|
93
103
|
if status == 401 or status == 403:
|
|
94
104
|
raise AuthError(body.get("detail") or str(body), status=status, body=body)
|
|
105
|
+
if status == 409 and body.get("error") == "consent_required":
|
|
106
|
+
raise ConsentRequired(
|
|
107
|
+
"consent_required",
|
|
108
|
+
body=body,
|
|
109
|
+
consent_request_id=body.get("consent_request_id"),
|
|
110
|
+
action=body.get("action"),
|
|
111
|
+
hold_id=body.get("hold_id"),
|
|
112
|
+
expires_at=body.get("expires_at"),
|
|
113
|
+
)
|
|
95
114
|
if status == 409:
|
|
96
115
|
raise Conflict(
|
|
97
116
|
body.get("error", "outranked"),
|
|
@@ -162,6 +181,9 @@ class _Bookings(_Resource):
|
|
|
162
181
|
buffer_after_min: int = 0,
|
|
163
182
|
bump_window_seconds: Optional[int] = None,
|
|
164
183
|
metadata: Optional[dict] = None,
|
|
184
|
+
with_conferencing: bool = False,
|
|
185
|
+
title: Optional[str] = None,
|
|
186
|
+
invitee: Optional[dict] = None,
|
|
165
187
|
) -> Booking:
|
|
166
188
|
return self._post(
|
|
167
189
|
start=start,
|
|
@@ -172,6 +194,9 @@ class _Bookings(_Resource):
|
|
|
172
194
|
buffer_after_min=buffer_after_min,
|
|
173
195
|
bump_window_seconds=bump_window_seconds,
|
|
174
196
|
metadata=metadata,
|
|
197
|
+
with_conferencing=with_conferencing,
|
|
198
|
+
title=title,
|
|
199
|
+
invitee=invitee,
|
|
175
200
|
)
|
|
176
201
|
|
|
177
202
|
def hold(
|
|
@@ -183,6 +208,8 @@ class _Bookings(_Resource):
|
|
|
183
208
|
buffer_before_min: int = 0,
|
|
184
209
|
buffer_after_min: int = 0,
|
|
185
210
|
metadata: Optional[dict] = None,
|
|
211
|
+
title: Optional[str] = None,
|
|
212
|
+
invitee: Optional[dict] = None,
|
|
186
213
|
) -> Booking:
|
|
187
214
|
return self._post(
|
|
188
215
|
start=start,
|
|
@@ -192,6 +219,8 @@ class _Bookings(_Resource):
|
|
|
192
219
|
buffer_before_min=buffer_before_min,
|
|
193
220
|
buffer_after_min=buffer_after_min,
|
|
194
221
|
metadata=metadata,
|
|
222
|
+
title=title,
|
|
223
|
+
invitee=invitee,
|
|
195
224
|
)
|
|
196
225
|
|
|
197
226
|
def release(self, hold_id: str) -> None:
|
|
@@ -214,6 +243,12 @@ class _Bookings(_Resource):
|
|
|
214
243
|
body["bump_window_seconds"] = kwargs["bump_window_seconds"]
|
|
215
244
|
if kwargs.get("metadata"):
|
|
216
245
|
body["metadata"] = kwargs["metadata"]
|
|
246
|
+
if kwargs.get("with_conferencing"):
|
|
247
|
+
body["with_conferencing"] = True
|
|
248
|
+
if kwargs.get("title"):
|
|
249
|
+
body["title"] = kwargs["title"]
|
|
250
|
+
if kwargs.get("invitee"):
|
|
251
|
+
body["invitee"] = kwargs["invitee"]
|
|
217
252
|
|
|
218
253
|
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
219
254
|
_, j, _ = self._c._request("POST", "/v1/bookings", json=body, headers=headers)
|
|
@@ -312,3 +347,84 @@ class _Mailbox(_Resource):
|
|
|
312
347
|
"GET", f"/v1/mailbox/suppressions/{quote(address, safe='@+.')}"
|
|
313
348
|
)
|
|
314
349
|
return j
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
TERMINAL_APPROVAL_STATUSES = frozenset({"approved", "denied", "expired"})
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class _Approvals(_Resource):
|
|
356
|
+
"""Human sign-off for anything your agent is about to do (#446).
|
|
357
|
+
|
|
358
|
+
The action need not be an AgentDraft one — gate a deploy, a refund, or an
|
|
359
|
+
outbound email on the same call. Requires the ``approvals:request`` scope.
|
|
360
|
+
|
|
361
|
+
approval = client.approvals.request(
|
|
362
|
+
action_type="deploy.production",
|
|
363
|
+
summary="Ship v2.3.0 to production",
|
|
364
|
+
evidence={"commit": "a1b2c3d", "migrations": 2},
|
|
365
|
+
)
|
|
366
|
+
result = client.approvals.wait(approval["approval_id"])
|
|
367
|
+
if result["status"] != "approved":
|
|
368
|
+
raise SystemExit(f"stood down: {result['status']}")
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
def request(
|
|
372
|
+
self,
|
|
373
|
+
action_type: str,
|
|
374
|
+
summary: str,
|
|
375
|
+
*,
|
|
376
|
+
evidence: Optional[dict] = None,
|
|
377
|
+
ttl_seconds: Optional[int] = None,
|
|
378
|
+
idempotency_key: Optional[str] = None,
|
|
379
|
+
) -> dict:
|
|
380
|
+
"""Open a request and return immediately with ``{approval_id, status,
|
|
381
|
+
expires_at}``. Nothing blocks until you call :meth:`wait`."""
|
|
382
|
+
body: dict[str, Any] = {"action_type": action_type, "summary": summary}
|
|
383
|
+
if evidence is not None:
|
|
384
|
+
body["evidence"] = evidence
|
|
385
|
+
if ttl_seconds is not None:
|
|
386
|
+
body["ttl_seconds"] = ttl_seconds
|
|
387
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
388
|
+
_, j, _ = self._c._request("POST", "/v1/approvals", json=body, headers=headers)
|
|
389
|
+
return j
|
|
390
|
+
|
|
391
|
+
def get(self, approval_id: str) -> dict:
|
|
392
|
+
"""Current state of a request, including its resolution once decided."""
|
|
393
|
+
_, j, _ = self._c._request("GET", f"/v1/approvals/{approval_id}")
|
|
394
|
+
return j
|
|
395
|
+
|
|
396
|
+
def wait(
|
|
397
|
+
self,
|
|
398
|
+
approval_id: str,
|
|
399
|
+
*,
|
|
400
|
+
timeout: Optional[float] = None,
|
|
401
|
+
poll_interval: float = 5.0,
|
|
402
|
+
) -> dict:
|
|
403
|
+
"""Block until a human decides, then return the resolved approval.
|
|
404
|
+
|
|
405
|
+
Returns on any terminal status — ``approved``, ``denied``, or
|
|
406
|
+
``expired`` — so check ``result["status"]`` rather than assuming a
|
|
407
|
+
return means yes.
|
|
408
|
+
|
|
409
|
+
``timeout`` is a client-side ceiling in seconds. Left unset, the wait
|
|
410
|
+
follows the request's own ``expires_at``: once it lapses the server
|
|
411
|
+
sweeps it to ``expired`` and this returns that, so the default never
|
|
412
|
+
loops forever. :class:`ApprovalTimeout` therefore only fires when *you*
|
|
413
|
+
set a timeout shorter than the request's life — the request stays
|
|
414
|
+
pending and resolvable when it does.
|
|
415
|
+
"""
|
|
416
|
+
started = time.monotonic()
|
|
417
|
+
deadline = started + timeout if timeout is not None else None
|
|
418
|
+
while True:
|
|
419
|
+
current = self.get(approval_id)
|
|
420
|
+
if current.get("status") in TERMINAL_APPROVAL_STATUSES:
|
|
421
|
+
return current
|
|
422
|
+
|
|
423
|
+
waited = time.monotonic() - started
|
|
424
|
+
if deadline is not None and time.monotonic() + poll_interval > deadline:
|
|
425
|
+
raise ApprovalTimeout(
|
|
426
|
+
f"approval {approval_id} still pending after {waited:.0f}s",
|
|
427
|
+
approval_id=approval_id,
|
|
428
|
+
waited_seconds=waited,
|
|
429
|
+
)
|
|
430
|
+
time.sleep(poll_interval)
|
|
@@ -46,6 +46,49 @@ class Conflict(AgentDraftError):
|
|
|
46
46
|
self.reason = reason
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
class ConsentRequired(AgentDraftError):
|
|
50
|
+
"""409 — a high-impact action is gated on a human approve/deny (#236).
|
|
51
|
+
|
|
52
|
+
The slot is parked as a HOLD until a person decides on the dashboard, so the
|
|
53
|
+
action is neither committed nor lost. ``consent_request_id`` identifies the
|
|
54
|
+
pending decision; ``hold_id`` is the parked hold; ``expires_at`` is when the
|
|
55
|
+
park (and the request) lapse if nobody acts. Distinct from ``Conflict``: you
|
|
56
|
+
did not lose to another agent — you're waiting on a human.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
message: str,
|
|
62
|
+
*,
|
|
63
|
+
status: int = 409,
|
|
64
|
+
body: dict,
|
|
65
|
+
consent_request_id: Optional[str] = None,
|
|
66
|
+
action: Optional[str] = None,
|
|
67
|
+
hold_id: Optional[str] = None,
|
|
68
|
+
expires_at: Optional[str] = None,
|
|
69
|
+
):
|
|
70
|
+
super().__init__(message, status=status, body=body)
|
|
71
|
+
self.consent_request_id = consent_request_id
|
|
72
|
+
self.action = action
|
|
73
|
+
self.hold_id = hold_id
|
|
74
|
+
self.expires_at = expires_at
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ApprovalTimeout(AgentDraftError):
|
|
78
|
+
"""``approvals.wait()`` gave up before a human decided (#446).
|
|
79
|
+
|
|
80
|
+
Client-side only — the request is still pending on the server and a human
|
|
81
|
+
can still resolve it, so treat this as "not yet", not "denied". Poll again
|
|
82
|
+
with :meth:`Client.approvals.get` or wait again; ``approval_id`` is carried
|
|
83
|
+
so you can.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, message: str, *, approval_id: str, waited_seconds: float):
|
|
87
|
+
super().__init__(message)
|
|
88
|
+
self.approval_id = approval_id
|
|
89
|
+
self.waited_seconds = waited_seconds
|
|
90
|
+
|
|
91
|
+
|
|
49
92
|
class RuleViolation(AgentDraftError):
|
|
50
93
|
"""422 — the proposed booking violates a rule (working hours, focus block, etc.)."""
|
|
51
94
|
|
|
@@ -19,14 +19,33 @@ class Booking:
|
|
|
19
19
|
status: str # "COMMITTED" | "HOLD"
|
|
20
20
|
expires_at: Optional[datetime] = None
|
|
21
21
|
audit_event_id: Optional[str] = None
|
|
22
|
+
# First-class booking identity (echoed back when you set them on commit/hold).
|
|
23
|
+
# `title` is the human-readable label; `invitee` is the `{name, email, notes}`
|
|
24
|
+
# of the person the slot is for. Both are None when not supplied.
|
|
25
|
+
title: Optional[str] = None
|
|
26
|
+
invitee: Optional[dict] = None
|
|
27
|
+
# Present only when the booking was committed with `with_conferencing=True`.
|
|
28
|
+
# `conferencing_provider` is e.g. "jitsi" or "google_meet". `conferencing_url`
|
|
29
|
+
# is None when a link could not be produced — `conferencing_status` then says
|
|
30
|
+
# why ("no_provider", "auth_expired", "api_error", "no_link_returned");
|
|
31
|
+
# "attached" means the url is populated.
|
|
32
|
+
conferencing_url: Optional[str] = None
|
|
33
|
+
conferencing_provider: Optional[str] = None
|
|
34
|
+
conferencing_status: Optional[str] = None
|
|
22
35
|
|
|
23
36
|
@classmethod
|
|
24
37
|
def from_json(cls, j: dict) -> "Booking":
|
|
38
|
+
conf = j.get("conferencing") or {}
|
|
25
39
|
return cls(
|
|
26
40
|
booking_id=j["booking_id"],
|
|
27
41
|
status=j["status"],
|
|
28
42
|
expires_at=_parse_dt(j.get("expires_at")),
|
|
29
43
|
audit_event_id=j.get("audit_event_id"),
|
|
44
|
+
title=j.get("title"),
|
|
45
|
+
invitee=j.get("invitee"),
|
|
46
|
+
conferencing_url=conf.get("url"),
|
|
47
|
+
conferencing_provider=conf.get("provider"),
|
|
48
|
+
conferencing_status=conf.get("status"),
|
|
30
49
|
)
|
|
31
50
|
|
|
32
51
|
|
|
@@ -53,6 +72,10 @@ class AgentIdentity:
|
|
|
53
72
|
user_id: str
|
|
54
73
|
priority: int
|
|
55
74
|
scopes: list[str]
|
|
75
|
+
# The owner's IANA timezone (e.g. "America/New_York"), so a client can render
|
|
76
|
+
# slots in the owner's zone instead of converting blind. "UTC" until the owner
|
|
77
|
+
# sets one in the dashboard.
|
|
78
|
+
timezone: str = "UTC"
|
|
56
79
|
|
|
57
80
|
@classmethod
|
|
58
81
|
def from_json(cls, j: dict) -> "AgentIdentity":
|
|
@@ -61,4 +84,5 @@ class AgentIdentity:
|
|
|
61
84
|
user_id=j["user_id"],
|
|
62
85
|
priority=j["priority"],
|
|
63
86
|
scopes=j.get("scopes", []),
|
|
87
|
+
timezone=j.get("timezone") or "UTC",
|
|
64
88
|
)
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentdraft
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: AgentDraft SDK — ops API for AI agents with inbox, conflict-free calendar booking, and audit trail.
|
|
5
5
|
Author-email: AgentDraft Labs <hello@agentdraft.io>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://agentdraft.io
|
|
8
8
|
Project-URL: Documentation, https://agentdraft.io/docs
|
|
9
9
|
Project-URL: Specification, https://agentdraft.io/spec
|
|
10
10
|
Project-URL: Changelog, https://agentdraft.io/changelog
|
|
11
|
+
Project-URL: Repository, https://github.com/ryabinski-labs/agentdraft
|
|
12
|
+
Project-URL: Issues, https://github.com/ryabinski-labs/agentdraft/issues
|
|
11
13
|
Keywords: agentdraft,ai-agents,agents,calendar,scheduling,booking,coordination,langchain,crewai,autogen,mcp
|
|
12
14
|
Classifier: Development Status :: 4 - Beta
|
|
13
15
|
Classifier: Intended Audience :: Developers
|
|
@@ -54,6 +56,14 @@ pip install agentdraft
|
|
|
54
56
|
|
|
55
57
|
Requires Python 3.9+.
|
|
56
58
|
|
|
59
|
+
## Upgrade
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install -U agentdraft
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Release notes: <https://agentdraft.io/changelog>
|
|
66
|
+
|
|
57
67
|
## Quickstart
|
|
58
68
|
|
|
59
69
|
```python
|
|
@@ -69,13 +79,21 @@ try:
|
|
|
69
79
|
booking = client.bookings.commit(
|
|
70
80
|
start=start, end=end,
|
|
71
81
|
idempotency_key="ik_call_42",
|
|
72
|
-
|
|
82
|
+
title="Discovery call",
|
|
83
|
+
invitee={"name": "Ada Lovelace", "email": "ada@example.com"},
|
|
73
84
|
)
|
|
74
85
|
print("booked:", booking.booking_id)
|
|
75
86
|
except Conflict as e:
|
|
76
87
|
print(f"outranked by {e.winning_agent_id} (rank {e.winning_agent_priority})")
|
|
77
88
|
```
|
|
78
89
|
|
|
90
|
+
`title` and `invitee` (`{name, email, notes}`, all optional) are first-class
|
|
91
|
+
fields: they're persisted on the booking, echoed back on the returned
|
|
92
|
+
`Booking`, and shown to the calendar owner in the dashboard. A top-level
|
|
93
|
+
`title` supersedes the legacy `metadata={"title": ...}`. `client.agents.me()`
|
|
94
|
+
also returns the owner's `timezone` so a client can render slots in their
|
|
95
|
+
zone.
|
|
96
|
+
|
|
79
97
|
A losing agent gets a typed `Conflict` exception, not a timeout — it
|
|
80
98
|
knows who won, by what priority, and where the audit row lives, so
|
|
81
99
|
fallback behavior (propose an alternate, escalate, defer) is a clean
|
|
@@ -104,7 +122,7 @@ configurable bump window.
|
|
|
104
122
|
The repo ships a multi-agent race demo. With the local stack up:
|
|
105
123
|
|
|
106
124
|
```bash
|
|
107
|
-
git clone https://github.com/
|
|
125
|
+
git clone https://github.com/ryabinski-labs/agentdraft && cd agentdraft
|
|
108
126
|
docker compose up -d dynamodb
|
|
109
127
|
pip install -e ".[dev]" -e sdks/python
|
|
110
128
|
uvicorn app.main:app --port 8080 &
|
|
@@ -177,8 +195,8 @@ wrappers is on the roadmap.
|
|
|
177
195
|
- Protocol spec: <https://agentdraft.io/spec>
|
|
178
196
|
- API docs: <https://agentdraft.io/docs>
|
|
179
197
|
- Changelog: <https://agentdraft.io/changelog>
|
|
180
|
-
- Source & issues: <https://github.com/
|
|
181
|
-
- TypeScript SDK: [
|
|
198
|
+
- Source & issues: <https://github.com/ryabinski-labs/agentdraft>
|
|
199
|
+
- TypeScript SDK: [`agentdraft`](https://www.npmjs.com/package/agentdraft)
|
|
182
200
|
|
|
183
201
|
## Security
|
|
184
202
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agentdraft"
|
|
3
|
-
version = "0.1.
|
|
4
|
-
description = "
|
|
3
|
+
version = "0.1.2"
|
|
4
|
+
description = "AgentDraft SDK — ops API for AI agents with inbox, conflict-free calendar booking, and audit trail."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.9"
|
|
7
7
|
license = { text = "MIT" }
|
|
@@ -40,6 +40,8 @@ Homepage = "https://agentdraft.io"
|
|
|
40
40
|
Documentation = "https://agentdraft.io/docs"
|
|
41
41
|
Specification = "https://agentdraft.io/spec"
|
|
42
42
|
Changelog = "https://agentdraft.io/changelog"
|
|
43
|
+
Repository = "https://github.com/ryabinski-labs/agentdraft"
|
|
44
|
+
Issues = "https://github.com/ryabinski-labs/agentdraft/issues"
|
|
43
45
|
|
|
44
46
|
[build-system]
|
|
45
47
|
requires = ["setuptools>=68"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|