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.
@@ -1,13 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentdraft
3
- Version: 0.1.1
4
- Summary: Coordination layer for AI scheduling agents — race-free multi-agent booking with priority ranking and an audit log.
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
- metadata={"title": "Discovery call"},
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/GipsyChef/agentdraft && cd agentdraft
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/GipsyChef/agentdraft>
181
- - TypeScript SDK: [`@agentdraft/sdk`](https://www.npmjs.com/package/@agentdraft/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
- metadata={"title": "Discovery call"},
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/GipsyChef/agentdraft && cd agentdraft
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/GipsyChef/agentdraft>
152
- - TypeScript SDK: [`@agentdraft/sdk`](https://www.npmjs.com/package/@agentdraft/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
- metadata={"title": "Discovery call"},
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.1"
27
+ __version__ = "0.1.2"
27
28
 
28
29
  from .client import Client
29
- from .errors import AgentDraftError, AuthError, Conflict, RateLimited, RuleViolation
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 AgentDraftError, AuthError, Conflict, RateLimited, RuleViolation
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.1
4
- Summary: Coordination layer for AI scheduling agents — race-free multi-agent booking with priority ranking and an audit log.
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
- metadata={"title": "Discovery call"},
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/GipsyChef/agentdraft && cd agentdraft
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/GipsyChef/agentdraft>
181
- - TypeScript SDK: [`@agentdraft/sdk`](https://www.npmjs.com/package/@agentdraft/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.1"
4
- description = "Coordination layer for AI scheduling agents — race-free multi-agent booking with priority ranking and an audit log."
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