agentdraft 0.1.0__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.2/PKG-INFO +208 -0
- agentdraft-0.1.2/README.md +177 -0
- {agentdraft-0.1.0 → agentdraft-0.1.2}/agentdraft/__init__.py +15 -4
- {agentdraft-0.1.0 → agentdraft-0.1.2}/agentdraft/client.py +120 -2
- {agentdraft-0.1.0 → agentdraft-0.1.2}/agentdraft/errors.py +43 -0
- {agentdraft-0.1.0 → agentdraft-0.1.2}/agentdraft/models.py +24 -0
- agentdraft-0.1.2/agentdraft.egg-info/PKG-INFO +208 -0
- {agentdraft-0.1.0 → agentdraft-0.1.2}/pyproject.toml +22 -6
- agentdraft-0.1.0/PKG-INFO +0 -147
- agentdraft-0.1.0/README.md +0 -120
- agentdraft-0.1.0/agentdraft.egg-info/PKG-INFO +0 -147
- {agentdraft-0.1.0 → agentdraft-0.1.2}/LICENSE +0 -0
- {agentdraft-0.1.0 → agentdraft-0.1.2}/agentdraft/py.typed +0 -0
- {agentdraft-0.1.0 → agentdraft-0.1.2}/agentdraft.egg-info/SOURCES.txt +0 -0
- {agentdraft-0.1.0 → agentdraft-0.1.2}/agentdraft.egg-info/dependency_links.txt +0 -0
- {agentdraft-0.1.0 → agentdraft-0.1.2}/agentdraft.egg-info/requires.txt +0 -0
- {agentdraft-0.1.0 → agentdraft-0.1.2}/agentdraft.egg-info/top_level.txt +0 -0
- {agentdraft-0.1.0 → agentdraft-0.1.2}/setup.cfg +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentdraft
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: AgentDraft SDK — ops API for AI agents with inbox, conflict-free calendar booking, and audit trail.
|
|
5
|
+
Author-email: AgentDraft Labs <hello@agentdraft.io>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://agentdraft.io
|
|
8
|
+
Project-URL: Documentation, https://agentdraft.io/docs
|
|
9
|
+
Project-URL: Specification, https://agentdraft.io/spec
|
|
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
|
|
13
|
+
Keywords: agentdraft,ai-agents,agents,calendar,scheduling,booking,coordination,langchain,crewai,autogen,mcp
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: Office/Business :: Scheduling
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: httpx>=0.27.0
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# agentdraft — Python SDK
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/agentdraft/)
|
|
35
|
+
[](https://pypi.org/project/agentdraft/)
|
|
36
|
+
[](https://agentdraft.io/legal/license)
|
|
37
|
+
|
|
38
|
+
**AgentDraft is the coordination layer for AI scheduling agents.** When two
|
|
39
|
+
or more agents — your sales bot, a Cal.com handler, an internal recruiter,
|
|
40
|
+
a Claude/OpenAI assistant — write to the same calendar, AgentDraft is the
|
|
41
|
+
one API that decides who wins, atomically, with a tamper-evident audit row
|
|
42
|
+
for every commit.
|
|
43
|
+
|
|
44
|
+
This is the official Python SDK. It gives an agent a typed, one-line
|
|
45
|
+
surface to participate.
|
|
46
|
+
|
|
47
|
+
- **Protocol spec:** <https://agentdraft.io/spec>
|
|
48
|
+
- **Why it exists:** <https://agentdraft.io/why>
|
|
49
|
+
- **Live API reference:** <https://agentdraft.io/docs>
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install agentdraft
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Requires Python 3.9+.
|
|
58
|
+
|
|
59
|
+
## Upgrade
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install -U agentdraft
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Release notes: <https://agentdraft.io/changelog>
|
|
66
|
+
|
|
67
|
+
## Quickstart
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from datetime import datetime, timedelta, timezone
|
|
71
|
+
from agentdraft import Client, Conflict
|
|
72
|
+
|
|
73
|
+
client = Client(api_key="avs_live_...") # or set AGENTDRAFT_API_KEY
|
|
74
|
+
|
|
75
|
+
start = datetime.now(timezone.utc) + timedelta(hours=4)
|
|
76
|
+
end = start + timedelta(minutes=30)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
booking = client.bookings.commit(
|
|
80
|
+
start=start, end=end,
|
|
81
|
+
idempotency_key="ik_call_42",
|
|
82
|
+
title="Discovery call",
|
|
83
|
+
invitee={"name": "Ada Lovelace", "email": "ada@example.com"},
|
|
84
|
+
)
|
|
85
|
+
print("booked:", booking.booking_id)
|
|
86
|
+
except Conflict as e:
|
|
87
|
+
print(f"outranked by {e.winning_agent_id} (rank {e.winning_agent_priority})")
|
|
88
|
+
```
|
|
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
|
+
|
|
97
|
+
A losing agent gets a typed `Conflict` exception, not a timeout — it
|
|
98
|
+
knows who won, by what priority, and where the audit row lives, so
|
|
99
|
+
fallback behavior (propose an alternate, escalate, defer) is a clean
|
|
100
|
+
`except` clause away.
|
|
101
|
+
|
|
102
|
+
## Why a separate API?
|
|
103
|
+
|
|
104
|
+
Coordinator frameworks (LangChain, LangGraph, CrewAI, AutoGen, Composio,
|
|
105
|
+
the OpenAI Agents SDK) coordinate *work between agents* — sequential,
|
|
106
|
+
parallel, or graph orchestration of LLM calls and tools. None of them
|
|
107
|
+
solve **write contention on the calendar itself**: two agents firing
|
|
108
|
+
`POST /events` against Google Calendar at the same moment will both
|
|
109
|
+
succeed, and you have a double-booking.
|
|
110
|
+
|
|
111
|
+
AgentDraft solves only that problem, and solves it once. Every agent
|
|
112
|
+
the calendar owner runs calls `bookings.commit(...)` against AgentDraft
|
|
113
|
+
before touching the real calendar. The conflict engine uses
|
|
114
|
+
time-bucketed conditional writes in DynamoDB inside a single
|
|
115
|
+
`TransactWriteItems` — atomic, race-free, no locks. The user ranks
|
|
116
|
+
their agents in the dashboard; ties go to the higher-rank agent;
|
|
117
|
+
recent commits can be evicted by higher-priority agents inside a
|
|
118
|
+
configurable bump window.
|
|
119
|
+
|
|
120
|
+
## See the race
|
|
121
|
+
|
|
122
|
+
The repo ships a multi-agent race demo. With the local stack up:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
git clone https://github.com/ryabinski-labs/agentdraft && cd agentdraft
|
|
126
|
+
docker compose up -d dynamodb
|
|
127
|
+
pip install -e ".[dev]" -e sdks/python
|
|
128
|
+
uvicorn app.main:app --port 8080 &
|
|
129
|
+
python scripts/demo_race.py # 5 agents, ranked priorities, one slot
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Five agents fire concurrently at the same target slot. The highest-rank
|
|
133
|
+
agent wins; the rest get `409 outranked` with the winner's identity for
|
|
134
|
+
graceful fallback. The demo prints per-agent latency, the winning
|
|
135
|
+
booking id, and a link to the audit trail of the whole race.
|
|
136
|
+
|
|
137
|
+
## Authentication
|
|
138
|
+
|
|
139
|
+
API keys are issued from the AgentDraft dashboard and start with
|
|
140
|
+
`avs_live_`. Pass it explicitly or let the client read it from the
|
|
141
|
+
environment:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
Client(api_key="avs_live_...")
|
|
145
|
+
# or
|
|
146
|
+
import os; os.environ["AGENTDRAFT_API_KEY"] = "avs_live_..."
|
|
147
|
+
Client()
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
For local development against a dev backend, point at it via
|
|
151
|
+
`AGENTDRAFT_BASE_URL` or the `base_url=` kwarg.
|
|
152
|
+
|
|
153
|
+
## Surface
|
|
154
|
+
|
|
155
|
+
| Attribute | Purpose |
|
|
156
|
+
|---|---|
|
|
157
|
+
| `client.availability` | Read merged availability across all agents writing to the calendar |
|
|
158
|
+
| `client.bookings` | `hold`, `release`, `commit`, `cancel` — the four state transitions |
|
|
159
|
+
| `client.agents` | `me()` — confirm key + current priority + scopes |
|
|
160
|
+
| `client.mailbox` | Inbound/outbound mail surface for agents that book via email |
|
|
161
|
+
|
|
162
|
+
All blocking I/O. An async client is on the roadmap; for now wrap with
|
|
163
|
+
`asyncio.to_thread` if you need concurrency.
|
|
164
|
+
|
|
165
|
+
## Error types
|
|
166
|
+
|
|
167
|
+
Every failure is a typed exception so callers can branch precisely:
|
|
168
|
+
|
|
169
|
+
- `Conflict` — your write was outranked. Carries `winning_booking_id`,
|
|
170
|
+
`winning_agent_id`, `winning_agent_priority`, `your_priority`, and
|
|
171
|
+
`reason`. The winning booking's `audit_event_id` is available on the
|
|
172
|
+
returned `Booking` model for the agent that *did* win.
|
|
173
|
+
- `AuthError` — bad / missing / expired API key.
|
|
174
|
+
- `RateLimited` — token bucket exhausted. Has `retry_after` (seconds).
|
|
175
|
+
- `RuleViolation` — request was syntactically valid but violated a
|
|
176
|
+
rule (focus block, daily cap, business hours).
|
|
177
|
+
- `AgentDraftError` — base class; catch this if you only need a
|
|
178
|
+
catch-all.
|
|
179
|
+
|
|
180
|
+
## Idempotency
|
|
181
|
+
|
|
182
|
+
Pass `idempotency_key=` to `bookings.commit(...)`. The server caches the
|
|
183
|
+
result by `(agent_id, key)` for 24 hours, so a retry over a flaky network
|
|
184
|
+
returns the original booking, not a duplicate.
|
|
185
|
+
|
|
186
|
+
## Use with LangChain / CrewAI / AutoGen
|
|
187
|
+
|
|
188
|
+
The SDK is framework-agnostic — wrap any method in a `Tool` and pass it
|
|
189
|
+
to your agent. A first-party `agentdraft-langchain` package with ready
|
|
190
|
+
`BookingTool` / `AvailabilityTool` / `ConflictAwareBookingTool`
|
|
191
|
+
wrappers is on the roadmap.
|
|
192
|
+
|
|
193
|
+
## Links
|
|
194
|
+
|
|
195
|
+
- Protocol spec: <https://agentdraft.io/spec>
|
|
196
|
+
- API docs: <https://agentdraft.io/docs>
|
|
197
|
+
- Changelog: <https://agentdraft.io/changelog>
|
|
198
|
+
- Source & issues: <https://github.com/ryabinski-labs/agentdraft>
|
|
199
|
+
- TypeScript SDK: [`agentdraft`](https://www.npmjs.com/package/agentdraft)
|
|
200
|
+
|
|
201
|
+
## Security
|
|
202
|
+
|
|
203
|
+
Found a vulnerability? See <https://agentdraft.io/security> — **do not**
|
|
204
|
+
open a public issue for a security report.
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT — see <https://agentdraft.io/legal/license>.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# agentdraft — Python SDK
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/agentdraft/)
|
|
4
|
+
[](https://pypi.org/project/agentdraft/)
|
|
5
|
+
[](https://agentdraft.io/legal/license)
|
|
6
|
+
|
|
7
|
+
**AgentDraft is the coordination layer for AI scheduling agents.** When two
|
|
8
|
+
or more agents — your sales bot, a Cal.com handler, an internal recruiter,
|
|
9
|
+
a Claude/OpenAI assistant — write to the same calendar, AgentDraft is the
|
|
10
|
+
one API that decides who wins, atomically, with a tamper-evident audit row
|
|
11
|
+
for every commit.
|
|
12
|
+
|
|
13
|
+
This is the official Python SDK. It gives an agent a typed, one-line
|
|
14
|
+
surface to participate.
|
|
15
|
+
|
|
16
|
+
- **Protocol spec:** <https://agentdraft.io/spec>
|
|
17
|
+
- **Why it exists:** <https://agentdraft.io/why>
|
|
18
|
+
- **Live API reference:** <https://agentdraft.io/docs>
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install agentdraft
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Requires Python 3.9+.
|
|
27
|
+
|
|
28
|
+
## Upgrade
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install -U agentdraft
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Release notes: <https://agentdraft.io/changelog>
|
|
35
|
+
|
|
36
|
+
## Quickstart
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from datetime import datetime, timedelta, timezone
|
|
40
|
+
from agentdraft import Client, Conflict
|
|
41
|
+
|
|
42
|
+
client = Client(api_key="avs_live_...") # or set AGENTDRAFT_API_KEY
|
|
43
|
+
|
|
44
|
+
start = datetime.now(timezone.utc) + timedelta(hours=4)
|
|
45
|
+
end = start + timedelta(minutes=30)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
booking = client.bookings.commit(
|
|
49
|
+
start=start, end=end,
|
|
50
|
+
idempotency_key="ik_call_42",
|
|
51
|
+
title="Discovery call",
|
|
52
|
+
invitee={"name": "Ada Lovelace", "email": "ada@example.com"},
|
|
53
|
+
)
|
|
54
|
+
print("booked:", booking.booking_id)
|
|
55
|
+
except Conflict as e:
|
|
56
|
+
print(f"outranked by {e.winning_agent_id} (rank {e.winning_agent_priority})")
|
|
57
|
+
```
|
|
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
|
+
|
|
66
|
+
A losing agent gets a typed `Conflict` exception, not a timeout — it
|
|
67
|
+
knows who won, by what priority, and where the audit row lives, so
|
|
68
|
+
fallback behavior (propose an alternate, escalate, defer) is a clean
|
|
69
|
+
`except` clause away.
|
|
70
|
+
|
|
71
|
+
## Why a separate API?
|
|
72
|
+
|
|
73
|
+
Coordinator frameworks (LangChain, LangGraph, CrewAI, AutoGen, Composio,
|
|
74
|
+
the OpenAI Agents SDK) coordinate *work between agents* — sequential,
|
|
75
|
+
parallel, or graph orchestration of LLM calls and tools. None of them
|
|
76
|
+
solve **write contention on the calendar itself**: two agents firing
|
|
77
|
+
`POST /events` against Google Calendar at the same moment will both
|
|
78
|
+
succeed, and you have a double-booking.
|
|
79
|
+
|
|
80
|
+
AgentDraft solves only that problem, and solves it once. Every agent
|
|
81
|
+
the calendar owner runs calls `bookings.commit(...)` against AgentDraft
|
|
82
|
+
before touching the real calendar. The conflict engine uses
|
|
83
|
+
time-bucketed conditional writes in DynamoDB inside a single
|
|
84
|
+
`TransactWriteItems` — atomic, race-free, no locks. The user ranks
|
|
85
|
+
their agents in the dashboard; ties go to the higher-rank agent;
|
|
86
|
+
recent commits can be evicted by higher-priority agents inside a
|
|
87
|
+
configurable bump window.
|
|
88
|
+
|
|
89
|
+
## See the race
|
|
90
|
+
|
|
91
|
+
The repo ships a multi-agent race demo. With the local stack up:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
git clone https://github.com/ryabinski-labs/agentdraft && cd agentdraft
|
|
95
|
+
docker compose up -d dynamodb
|
|
96
|
+
pip install -e ".[dev]" -e sdks/python
|
|
97
|
+
uvicorn app.main:app --port 8080 &
|
|
98
|
+
python scripts/demo_race.py # 5 agents, ranked priorities, one slot
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Five agents fire concurrently at the same target slot. The highest-rank
|
|
102
|
+
agent wins; the rest get `409 outranked` with the winner's identity for
|
|
103
|
+
graceful fallback. The demo prints per-agent latency, the winning
|
|
104
|
+
booking id, and a link to the audit trail of the whole race.
|
|
105
|
+
|
|
106
|
+
## Authentication
|
|
107
|
+
|
|
108
|
+
API keys are issued from the AgentDraft dashboard and start with
|
|
109
|
+
`avs_live_`. Pass it explicitly or let the client read it from the
|
|
110
|
+
environment:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
Client(api_key="avs_live_...")
|
|
114
|
+
# or
|
|
115
|
+
import os; os.environ["AGENTDRAFT_API_KEY"] = "avs_live_..."
|
|
116
|
+
Client()
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
For local development against a dev backend, point at it via
|
|
120
|
+
`AGENTDRAFT_BASE_URL` or the `base_url=` kwarg.
|
|
121
|
+
|
|
122
|
+
## Surface
|
|
123
|
+
|
|
124
|
+
| Attribute | Purpose |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `client.availability` | Read merged availability across all agents writing to the calendar |
|
|
127
|
+
| `client.bookings` | `hold`, `release`, `commit`, `cancel` — the four state transitions |
|
|
128
|
+
| `client.agents` | `me()` — confirm key + current priority + scopes |
|
|
129
|
+
| `client.mailbox` | Inbound/outbound mail surface for agents that book via email |
|
|
130
|
+
|
|
131
|
+
All blocking I/O. An async client is on the roadmap; for now wrap with
|
|
132
|
+
`asyncio.to_thread` if you need concurrency.
|
|
133
|
+
|
|
134
|
+
## Error types
|
|
135
|
+
|
|
136
|
+
Every failure is a typed exception so callers can branch precisely:
|
|
137
|
+
|
|
138
|
+
- `Conflict` — your write was outranked. Carries `winning_booking_id`,
|
|
139
|
+
`winning_agent_id`, `winning_agent_priority`, `your_priority`, and
|
|
140
|
+
`reason`. The winning booking's `audit_event_id` is available on the
|
|
141
|
+
returned `Booking` model for the agent that *did* win.
|
|
142
|
+
- `AuthError` — bad / missing / expired API key.
|
|
143
|
+
- `RateLimited` — token bucket exhausted. Has `retry_after` (seconds).
|
|
144
|
+
- `RuleViolation` — request was syntactically valid but violated a
|
|
145
|
+
rule (focus block, daily cap, business hours).
|
|
146
|
+
- `AgentDraftError` — base class; catch this if you only need a
|
|
147
|
+
catch-all.
|
|
148
|
+
|
|
149
|
+
## Idempotency
|
|
150
|
+
|
|
151
|
+
Pass `idempotency_key=` to `bookings.commit(...)`. The server caches the
|
|
152
|
+
result by `(agent_id, key)` for 24 hours, so a retry over a flaky network
|
|
153
|
+
returns the original booking, not a duplicate.
|
|
154
|
+
|
|
155
|
+
## Use with LangChain / CrewAI / AutoGen
|
|
156
|
+
|
|
157
|
+
The SDK is framework-agnostic — wrap any method in a `Tool` and pass it
|
|
158
|
+
to your agent. A first-party `agentdraft-langchain` package with ready
|
|
159
|
+
`BookingTool` / `AvailabilityTool` / `ConflictAwareBookingTool`
|
|
160
|
+
wrappers is on the roadmap.
|
|
161
|
+
|
|
162
|
+
## Links
|
|
163
|
+
|
|
164
|
+
- Protocol spec: <https://agentdraft.io/spec>
|
|
165
|
+
- API docs: <https://agentdraft.io/docs>
|
|
166
|
+
- Changelog: <https://agentdraft.io/changelog>
|
|
167
|
+
- Source & issues: <https://github.com/ryabinski-labs/agentdraft>
|
|
168
|
+
- TypeScript SDK: [`agentdraft`](https://www.npmjs.com/package/agentdraft)
|
|
169
|
+
|
|
170
|
+
## Security
|
|
171
|
+
|
|
172
|
+
Found a vulnerability? See <https://agentdraft.io/security> — **do not**
|
|
173
|
+
open a public issue for a security report.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT — see <https://agentdraft.io/legal/license>.
|
|
@@ -16,27 +16,38 @@ 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
|
|
|
27
|
+
__version__ = "0.1.2"
|
|
28
|
+
|
|
26
29
|
from .client import Client
|
|
27
|
-
from .errors import
|
|
30
|
+
from .errors import (
|
|
31
|
+
AgentDraftError,
|
|
32
|
+
ApprovalTimeout,
|
|
33
|
+
AuthError,
|
|
34
|
+
Conflict,
|
|
35
|
+
ConsentRequired,
|
|
36
|
+
RateLimited,
|
|
37
|
+
RuleViolation,
|
|
38
|
+
)
|
|
28
39
|
from .models import Booking, Hold, Slot
|
|
29
40
|
|
|
30
41
|
__all__ = [
|
|
31
42
|
"Client",
|
|
32
43
|
"AgentDraftError",
|
|
44
|
+
"ApprovalTimeout",
|
|
33
45
|
"AuthError",
|
|
34
46
|
"Conflict",
|
|
47
|
+
"ConsentRequired",
|
|
35
48
|
"RateLimited",
|
|
36
49
|
"RuleViolation",
|
|
37
50
|
"Booking",
|
|
38
51
|
"Hold",
|
|
39
52
|
"Slot",
|
|
40
53
|
]
|
|
41
|
-
|
|
42
|
-
__version__ = "0.1.0"
|
|
@@ -3,15 +3,26 @@
|
|
|
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
|
-
from .
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .errors import (
|
|
14
|
+
AgentDraftError,
|
|
15
|
+
ApprovalTimeout,
|
|
16
|
+
AuthError,
|
|
17
|
+
Conflict,
|
|
18
|
+
ConsentRequired,
|
|
19
|
+
RateLimited,
|
|
20
|
+
RuleViolation,
|
|
21
|
+
)
|
|
12
22
|
from .models import AgentIdentity, Booking, Slot
|
|
13
23
|
|
|
14
24
|
DEFAULT_BASE_URL = "https://api.agentdraft.io"
|
|
25
|
+
DEFAULT_USER_AGENT = f"agentdraft-python/{__version__}"
|
|
15
26
|
|
|
16
27
|
|
|
17
28
|
class Client:
|
|
@@ -37,7 +48,7 @@ class Client:
|
|
|
37
48
|
*,
|
|
38
49
|
base_url: Optional[str] = None,
|
|
39
50
|
timeout: float = 10.0,
|
|
40
|
-
user_agent: str =
|
|
51
|
+
user_agent: str = DEFAULT_USER_AGENT,
|
|
41
52
|
):
|
|
42
53
|
api_key = api_key or os.environ.get("AGENTDRAFT_API_KEY")
|
|
43
54
|
if not api_key:
|
|
@@ -55,6 +66,7 @@ class Client:
|
|
|
55
66
|
self.bookings = _Bookings(self)
|
|
56
67
|
self.agents = _Agents(self)
|
|
57
68
|
self.mailbox = _Mailbox(self)
|
|
69
|
+
self.approvals = _Approvals(self)
|
|
58
70
|
|
|
59
71
|
def close(self) -> None:
|
|
60
72
|
self._http.close()
|
|
@@ -90,6 +102,15 @@ class Client:
|
|
|
90
102
|
def _raise_for(self, status: int, body: dict, headers: dict) -> None:
|
|
91
103
|
if status == 401 or status == 403:
|
|
92
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
|
+
)
|
|
93
114
|
if status == 409:
|
|
94
115
|
raise Conflict(
|
|
95
116
|
body.get("error", "outranked"),
|
|
@@ -160,6 +181,9 @@ class _Bookings(_Resource):
|
|
|
160
181
|
buffer_after_min: int = 0,
|
|
161
182
|
bump_window_seconds: Optional[int] = None,
|
|
162
183
|
metadata: Optional[dict] = None,
|
|
184
|
+
with_conferencing: bool = False,
|
|
185
|
+
title: Optional[str] = None,
|
|
186
|
+
invitee: Optional[dict] = None,
|
|
163
187
|
) -> Booking:
|
|
164
188
|
return self._post(
|
|
165
189
|
start=start,
|
|
@@ -170,6 +194,9 @@ class _Bookings(_Resource):
|
|
|
170
194
|
buffer_after_min=buffer_after_min,
|
|
171
195
|
bump_window_seconds=bump_window_seconds,
|
|
172
196
|
metadata=metadata,
|
|
197
|
+
with_conferencing=with_conferencing,
|
|
198
|
+
title=title,
|
|
199
|
+
invitee=invitee,
|
|
173
200
|
)
|
|
174
201
|
|
|
175
202
|
def hold(
|
|
@@ -181,6 +208,8 @@ class _Bookings(_Resource):
|
|
|
181
208
|
buffer_before_min: int = 0,
|
|
182
209
|
buffer_after_min: int = 0,
|
|
183
210
|
metadata: Optional[dict] = None,
|
|
211
|
+
title: Optional[str] = None,
|
|
212
|
+
invitee: Optional[dict] = None,
|
|
184
213
|
) -> Booking:
|
|
185
214
|
return self._post(
|
|
186
215
|
start=start,
|
|
@@ -190,6 +219,8 @@ class _Bookings(_Resource):
|
|
|
190
219
|
buffer_before_min=buffer_before_min,
|
|
191
220
|
buffer_after_min=buffer_after_min,
|
|
192
221
|
metadata=metadata,
|
|
222
|
+
title=title,
|
|
223
|
+
invitee=invitee,
|
|
193
224
|
)
|
|
194
225
|
|
|
195
226
|
def release(self, hold_id: str) -> None:
|
|
@@ -212,6 +243,12 @@ class _Bookings(_Resource):
|
|
|
212
243
|
body["bump_window_seconds"] = kwargs["bump_window_seconds"]
|
|
213
244
|
if kwargs.get("metadata"):
|
|
214
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"]
|
|
215
252
|
|
|
216
253
|
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
217
254
|
_, j, _ = self._c._request("POST", "/v1/bookings", json=body, headers=headers)
|
|
@@ -310,3 +347,84 @@ class _Mailbox(_Resource):
|
|
|
310
347
|
"GET", f"/v1/mailbox/suppressions/{quote(address, safe='@+.')}"
|
|
311
348
|
)
|
|
312
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
|
|