aipelago-sdk 0.6.1__py3-none-any.whl
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.
- aipelago/sdk/__init__.py +36 -0
- aipelago/sdk/context.py +800 -0
- aipelago/sdk/decorator.py +60 -0
- aipelago/sdk/hook.py +81 -0
- aipelago/sdk/principal.py +66 -0
- aipelago_sdk-0.6.1.dist-info/METADATA +30 -0
- aipelago_sdk-0.6.1.dist-info/RECORD +9 -0
- aipelago_sdk-0.6.1.dist-info/WHEEL +4 -0
- aipelago_sdk-0.6.1.dist-info/licenses/LICENSE +202 -0
aipelago/sdk/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Aipelago SDK — author-facing API.
|
|
2
|
+
|
|
3
|
+
What you import as an agent author. The kernel injects a `ToolContext`
|
|
4
|
+
into every tool handler; the SDK is the only surface authors should depend on.
|
|
5
|
+
|
|
6
|
+
It needs nothing else of the kernel, so it installs on its own as the
|
|
7
|
+
`aipelago-sdk` package: the one import every distribution's agents are written in.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from aipelago.sdk.context import (
|
|
11
|
+
AgentBuilderHandle,
|
|
12
|
+
AskReply,
|
|
13
|
+
EmbeddingHandle,
|
|
14
|
+
InboundAttachment,
|
|
15
|
+
SessionHandle,
|
|
16
|
+
SharesHandle,
|
|
17
|
+
ToolContext,
|
|
18
|
+
)
|
|
19
|
+
from aipelago.sdk.decorator import tool
|
|
20
|
+
from aipelago.sdk.hook import HookDecision, hook
|
|
21
|
+
from aipelago.sdk.principal import Principal, PrincipalStatus
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"AgentBuilderHandle",
|
|
25
|
+
"AskReply",
|
|
26
|
+
"EmbeddingHandle",
|
|
27
|
+
"HookDecision",
|
|
28
|
+
"InboundAttachment",
|
|
29
|
+
"Principal",
|
|
30
|
+
"PrincipalStatus",
|
|
31
|
+
"SessionHandle",
|
|
32
|
+
"SharesHandle",
|
|
33
|
+
"ToolContext",
|
|
34
|
+
"hook",
|
|
35
|
+
"tool",
|
|
36
|
+
]
|
aipelago/sdk/context.py
ADDED
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
"""`ToolContext` — the only object a tool handler may rely on.
|
|
2
|
+
|
|
3
|
+
The kernel constructs this per tool call and passes it as the first argument to
|
|
4
|
+
the handler. Crucially, it carries no raw DB connection, no provider client, no
|
|
5
|
+
`os` handle — every effect goes through a mediated method.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Awaitable, Callable
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Protocol
|
|
15
|
+
from uuid import UUID
|
|
16
|
+
|
|
17
|
+
from aipelago.sdk.principal import Principal
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class AskReply:
|
|
22
|
+
"""What came back from the person `ctx.ask` asked.
|
|
23
|
+
|
|
24
|
+
`text` is always the whole answer in prose. It is what a chat channel
|
|
25
|
+
produced, what the trajectory records, and what a yes/no decision is judged
|
|
26
|
+
on — every path can fill it, so every caller can read it.
|
|
27
|
+
|
|
28
|
+
`values` is the same answer keyed by field, and only a form filled in on a
|
|
29
|
+
surface that could draw one produces it. It is the half worth having when
|
|
30
|
+
there were several questions: without it the caller gets four answers glued
|
|
31
|
+
into one string and has to take them apart again, which is the parsing step
|
|
32
|
+
the form existed to remove. Empty when the answer arrived as plain text.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
text: str
|
|
36
|
+
values: dict[str, list[str]] = field(default_factory=dict)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SessionHandle(Protocol):
|
|
40
|
+
"""Ephemeral per-session scratch, exposed as `ctx.session`."""
|
|
41
|
+
|
|
42
|
+
def get(self, key: str, default: Any = None) -> Any: ...
|
|
43
|
+
def set(self, key: str, value: Any) -> None: ...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class InboundAttachment(Protocol):
|
|
47
|
+
"""One of the caller's attachments as `ctx.attachment()` returns it — bytes
|
|
48
|
+
plus enough to interpret them."""
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def media_type(self) -> str: ...
|
|
52
|
+
@property
|
|
53
|
+
def data(self) -> bytes: ...
|
|
54
|
+
@property
|
|
55
|
+
def filename(self) -> str: ...
|
|
56
|
+
@property
|
|
57
|
+
def id(self) -> UUID | None: ...
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
#: Ask the user a free-form question (optionally with suggested choices) and wait
|
|
61
|
+
#: for their reply over the active channel. Returns the reply text, or None if no
|
|
62
|
+
#: interactive channel is attached or the wait times out.
|
|
63
|
+
AskFn = Callable[[str, "list[str] | None"], Awaitable["str | None"]]
|
|
64
|
+
|
|
65
|
+
#: Fetch one of the caller's own attachments by id. Returns the attachment
|
|
66
|
+
#: (bytes + media_type + filename), or None when no attachment of this caller
|
|
67
|
+
#: has that id — "not yours" and "does not exist" look the same.
|
|
68
|
+
AttachmentFn = Callable[[str], Awaitable["InboundAttachment | None"]]
|
|
69
|
+
|
|
70
|
+
#: Answers `ctx.is_admin()`. Three-valued on purpose — see the method.
|
|
71
|
+
AdminFn = Callable[[], Awaitable["bool | None"]]
|
|
72
|
+
#: Behind `ctx.notify`: the notice body in, the receipt out.
|
|
73
|
+
NotifyFn = Callable[[dict[str, Any]], Awaitable[dict[str, Any]]]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class _StateHandle(Protocol):
|
|
77
|
+
async def execute(self, query: str, *args: Any) -> str: ...
|
|
78
|
+
async def fetch(self, query: str, *args: Any) -> list[Any]: ...
|
|
79
|
+
async def fetchrow(self, query: str, *args: Any) -> Any: ...
|
|
80
|
+
async def fetchval(self, query: str, *args: Any) -> Any: ...
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class CapabilityHandle:
|
|
84
|
+
"""Capability invocation handle exposed via `ctx.capability`."""
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
check: Callable[[str], Awaitable[bool]],
|
|
89
|
+
invoke: Callable[[str, dict[str, Any]], Awaitable[Any]],
|
|
90
|
+
) -> None:
|
|
91
|
+
self._check = check
|
|
92
|
+
self._invoke = invoke
|
|
93
|
+
|
|
94
|
+
async def check(self, capability_id: str) -> bool:
|
|
95
|
+
return await self._check(capability_id)
|
|
96
|
+
|
|
97
|
+
async def invoke(self, capability_id: str, args: dict[str, Any]) -> Any:
|
|
98
|
+
return await self._invoke(capability_id, args)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ChannelHandle:
|
|
102
|
+
"""Channel send handle exposed via `ctx.channel`."""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
send: Callable[..., Awaitable[Any]],
|
|
107
|
+
) -> None:
|
|
108
|
+
self._send = send
|
|
109
|
+
|
|
110
|
+
async def send(self, *, to: Any, message: str, via: str | None = None) -> Any:
|
|
111
|
+
return await self._send(to=to, message=message, via=via)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class BudgetHandle:
|
|
115
|
+
"""Per-invocation budget control."""
|
|
116
|
+
|
|
117
|
+
def __init__(self, charge: Callable[[dict[str, Any]], Awaitable[None]]) -> None:
|
|
118
|
+
self._charge = charge
|
|
119
|
+
|
|
120
|
+
async def charge(self, cost: dict[str, Any]) -> None:
|
|
121
|
+
await self._charge(cost)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class EmbeddingHandle:
|
|
125
|
+
"""Provider-neutral embedding seam exposed to agent tools."""
|
|
126
|
+
|
|
127
|
+
def __init__(self, registry: Any) -> None:
|
|
128
|
+
self._registry = registry
|
|
129
|
+
|
|
130
|
+
def describe(self) -> dict[str, Any]:
|
|
131
|
+
if self._registry is None:
|
|
132
|
+
return {"available": False}
|
|
133
|
+
name = self._registry.select()
|
|
134
|
+
if name is None:
|
|
135
|
+
return {"available": False}
|
|
136
|
+
provider = self._registry.get(name)
|
|
137
|
+
metadata = dict(provider.meta)
|
|
138
|
+
describe = metadata.pop("describe", None)
|
|
139
|
+
if callable(describe):
|
|
140
|
+
metadata.update(describe())
|
|
141
|
+
return {
|
|
142
|
+
"available": True,
|
|
143
|
+
"provider": name,
|
|
144
|
+
"dimensions": metadata.pop("dimensions", metadata.pop("dim", None)),
|
|
145
|
+
"semantic": name != "hashing",
|
|
146
|
+
**metadata,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async def embed(self, texts: list[str], dimensions: int | None = None) -> list[list[float]]:
|
|
150
|
+
if self._registry is None:
|
|
151
|
+
raise RuntimeError("embedding backend unavailable")
|
|
152
|
+
name = self._registry.select()
|
|
153
|
+
if name is None:
|
|
154
|
+
raise RuntimeError("embedding backend unavailable")
|
|
155
|
+
return await self._registry.get(name).run(texts, dimensions)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ScheduleHandle:
|
|
159
|
+
"""Deferred/recurring self-scheduling exposed via `ctx.schedule`.
|
|
160
|
+
|
|
161
|
+
Lets a tool register a future persona invocation (a reminder, a recurring
|
|
162
|
+
job) that the kernel fires and delivers later — the real primitive behind
|
|
163
|
+
"remind me in 15 minutes". Absent (None) inside an autonomous scheduled run,
|
|
164
|
+
so a fired task cannot schedule more (no recursion)."""
|
|
165
|
+
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
set_fn: Callable[..., Awaitable[Any]],
|
|
169
|
+
list_fn: Callable[[], Awaitable[Any]],
|
|
170
|
+
cancel_fn: Callable[[str], Awaitable[bool]],
|
|
171
|
+
) -> None:
|
|
172
|
+
self._set = set_fn
|
|
173
|
+
self._list = list_fn
|
|
174
|
+
self._cancel = cancel_fn
|
|
175
|
+
|
|
176
|
+
async def set(
|
|
177
|
+
self, *, when: str, kind: str = "notify", message: str = "", label: str = ""
|
|
178
|
+
) -> Any:
|
|
179
|
+
return await self._set(when=when, kind=kind, message=message, label=label)
|
|
180
|
+
|
|
181
|
+
async def list(self) -> Any:
|
|
182
|
+
return await self._list()
|
|
183
|
+
|
|
184
|
+
async def cancel(self, task_id: str) -> bool:
|
|
185
|
+
return await self._cancel(task_id)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class DispatchHandle:
|
|
189
|
+
"""Handing work to a PEER agent, exposed via `ctx.dispatch`.
|
|
190
|
+
|
|
191
|
+
Not a call into the peer's code — the kernel starts the peer's OWN persona
|
|
192
|
+
on the brief, with its own prompt, model and tools. Handed over, the work is
|
|
193
|
+
the peer's: the person hears from it directly, and its answer — what it
|
|
194
|
+
said when it finished — comes back to the sender through `status`. Asked
|
|
195
|
+
(`hand_off(wait=True)`), the sender waits for that answer inside its own
|
|
196
|
+
turn, which pays for it.
|
|
197
|
+
|
|
198
|
+
Absent (None) when this run may not hand work to a peer — for example when
|
|
199
|
+
it acts for no person.
|
|
200
|
+
|
|
201
|
+
Also how a run that IS a handed-over task sends it back — see `decline`.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
def __init__(
|
|
205
|
+
self,
|
|
206
|
+
hand_off_fn: Callable[..., Awaitable[Any]],
|
|
207
|
+
status_fn: Callable[[], Awaitable[Any]],
|
|
208
|
+
decline_fn: Callable[[str], Awaitable[None]] | None = None,
|
|
209
|
+
find_fn: Callable[[str], Awaitable[Any]] | None = None,
|
|
210
|
+
) -> None:
|
|
211
|
+
self._hand_off = hand_off_fn
|
|
212
|
+
self._status = status_fn
|
|
213
|
+
self._decline = decline_fn
|
|
214
|
+
self._find = find_fn
|
|
215
|
+
|
|
216
|
+
async def hand_off(
|
|
217
|
+
self,
|
|
218
|
+
*,
|
|
219
|
+
to_agent: str,
|
|
220
|
+
brief: str,
|
|
221
|
+
label: str = "",
|
|
222
|
+
forward_user_turn: bool = True,
|
|
223
|
+
wait: bool = False,
|
|
224
|
+
) -> Any:
|
|
225
|
+
"""Returns a RECEIPT (which peer, which handoff id, its status).
|
|
226
|
+
|
|
227
|
+
`wait=True` asks rather than hands over: the receipt comes back once the
|
|
228
|
+
peer has answered, with its `answer`, and the peer's run spends against
|
|
229
|
+
this run's own turn. A peer that cannot start now, or is still working
|
|
230
|
+
when this turn's own limits run out, carries on as an ordinary handoff;
|
|
231
|
+
its answer is then read later through `status`.
|
|
232
|
+
|
|
233
|
+
`forward_user_turn` sends the person's own message and its attachments
|
|
234
|
+
along with the brief. On by default, and the caller can only turn it OFF
|
|
235
|
+
— it cannot choose which turn, edit the words, or substitute the files.
|
|
236
|
+
The runtime reads them off the message this run was actually given, so
|
|
237
|
+
the receiver's "what the person sent" really is that.
|
|
238
|
+
"""
|
|
239
|
+
return await self._hand_off(
|
|
240
|
+
to_agent=to_agent,
|
|
241
|
+
brief=brief,
|
|
242
|
+
label=label,
|
|
243
|
+
forward_user_turn=forward_user_turn,
|
|
244
|
+
wait=wait,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
async def status(self) -> Any:
|
|
248
|
+
"""What this agent has handed off for this person lately, how it went,
|
|
249
|
+
and each peer's answer: what it said when it finished, or why not."""
|
|
250
|
+
return await self._status()
|
|
251
|
+
|
|
252
|
+
async def find(self, need: str) -> Any:
|
|
253
|
+
"""Peers whose own words look like `need`, best first.
|
|
254
|
+
|
|
255
|
+
The prompt names only some of the person's other agents; this searches
|
|
256
|
+
all of them, and finds none the person could not use.
|
|
257
|
+
"""
|
|
258
|
+
if self._find is None:
|
|
259
|
+
return []
|
|
260
|
+
return await self._find(need)
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def can_decline(self) -> bool:
|
|
264
|
+
"""True only inside a run that IS a handed-over piece of work.
|
|
265
|
+
|
|
266
|
+
There is nothing to decline anywhere else, and a tool that accepted the
|
|
267
|
+
word regardless would let a model believe it had refused something.
|
|
268
|
+
"""
|
|
269
|
+
return self._decline is not None
|
|
270
|
+
|
|
271
|
+
async def decline(self, reason: str) -> None:
|
|
272
|
+
"""Send this brief back: not work this agent does, and here is why.
|
|
273
|
+
|
|
274
|
+
Ends the handoff as `declined` — a terminal state that is neither
|
|
275
|
+
success nor failure. The kernel records the reason, tells the person, and
|
|
276
|
+
makes it visible to the sender through `status`. Declining in prose alone
|
|
277
|
+
would settle the handoff as `done`, which is the sender being told the
|
|
278
|
+
work happened.
|
|
279
|
+
"""
|
|
280
|
+
if self._decline is None:
|
|
281
|
+
raise ValueError("nothing to decline: this run is not a handed-over task")
|
|
282
|
+
await self._decline(reason)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
_Call = Callable[[str, dict[str, Any]], Awaitable[dict[str, Any]]]
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class MemoryHandle:
|
|
289
|
+
"""What this agent remembers about the person it acts for, via `ctx.memory`:
|
|
290
|
+
facts and standing rules that change how it helps, one sentence under a key.
|
|
291
|
+
Kept by the kernel per (agent, person), shown to the person in their settings.
|
|
292
|
+
|
|
293
|
+
Every method acts for the principal this context was built for; there is no
|
|
294
|
+
argument that names another, and no agent can read another agent's memory.
|
|
295
|
+
Returns plain dicts; a mistake the caller can fix raises `ValueError`. None
|
|
296
|
+
for system principals."""
|
|
297
|
+
|
|
298
|
+
def __init__(self, call: _Call) -> None:
|
|
299
|
+
self._call = call
|
|
300
|
+
|
|
301
|
+
async def list(self) -> dict[str, Any]:
|
|
302
|
+
return await self._call("list", {})
|
|
303
|
+
|
|
304
|
+
async def recall(self, query: str) -> dict[str, Any]:
|
|
305
|
+
return await self._call("recall", {"query": query})
|
|
306
|
+
|
|
307
|
+
async def save(self, key: str, content: str) -> dict[str, Any]:
|
|
308
|
+
"""Save or overwrite the memory under `key`."""
|
|
309
|
+
return await self._call("save", {"key": key, "content": content})
|
|
310
|
+
|
|
311
|
+
async def forget(self, key: str) -> dict[str, Any]:
|
|
312
|
+
return await self._call("forget", {"key": key})
|
|
313
|
+
|
|
314
|
+
async def history(self, id: str | None = None, *, limit: int = 30) -> dict[str, Any]:
|
|
315
|
+
return await self._call("history", {"id": id, "limit": limit})
|
|
316
|
+
|
|
317
|
+
async def undo(self, change_id: str) -> dict[str, Any]:
|
|
318
|
+
return await self._call("undo", {"change_id": change_id})
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class TodoHandle:
|
|
322
|
+
"""The person's to-dos with this agent, via `ctx.todos`: a title, a note, an
|
|
323
|
+
optional `due` date (2026-09-25) with `due_time` (18:30), `remind` and
|
|
324
|
+
`repeat` (daily/weekly/monthly/yearly), done or not, and optionally the
|
|
325
|
+
`item_id` it is about with `on_done={label, value}` written to that item when
|
|
326
|
+
it is finished. The person sees and edits them. Same rules as `MemoryHandle`."""
|
|
327
|
+
|
|
328
|
+
def __init__(self, call: _Call) -> None:
|
|
329
|
+
self._call = call
|
|
330
|
+
|
|
331
|
+
async def list(self, status: str = "open", query: str = "") -> dict[str, Any]:
|
|
332
|
+
"""`status`: open, done or all."""
|
|
333
|
+
return await self._call("list", {"status": status, "query": query})
|
|
334
|
+
|
|
335
|
+
async def add(self, title: str, **fields: Any) -> dict[str, Any]:
|
|
336
|
+
return await self._call("add", {"title": title, **fields})
|
|
337
|
+
|
|
338
|
+
async def update(self, id: str, **changes: Any) -> dict[str, Any]:
|
|
339
|
+
return await self._call("update", {"id": id, **changes})
|
|
340
|
+
|
|
341
|
+
async def done(self, id: str, done: bool = True) -> dict[str, Any]:
|
|
342
|
+
return await self._call("done", {"id": id, "done": done})
|
|
343
|
+
|
|
344
|
+
async def delete(self, id: str) -> dict[str, Any]:
|
|
345
|
+
return await self._call("delete", {"id": id})
|
|
346
|
+
|
|
347
|
+
async def history(self, id: str | None = None, *, limit: int = 30) -> dict[str, Any]:
|
|
348
|
+
return await self._call("history", {"id": id, "limit": limit})
|
|
349
|
+
|
|
350
|
+
async def undo(self, change_id: str) -> dict[str, Any]:
|
|
351
|
+
return await self._call("undo", {"change_id": change_id})
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class ItemHandle:
|
|
355
|
+
"""What the person keeps with this agent to look up, via `ctx.items`: each item
|
|
356
|
+
a title, a free-text `category`, and ordered `fields` —
|
|
357
|
+
`[{label, value, kind}]` with kind text, longtext, date, secret, url, phone or
|
|
358
|
+
number (`remind: true` on a date reminds the person). No schema to declare.
|
|
359
|
+
`update` merges fields by label, `remove` drops labels, and `note` adds a line
|
|
360
|
+
of what was done to the item's history. Same rules as `MemoryHandle`."""
|
|
361
|
+
|
|
362
|
+
def __init__(self, call: _Call) -> None:
|
|
363
|
+
self._call = call
|
|
364
|
+
|
|
365
|
+
async def list(self, category: str = "", query: str = "") -> dict[str, Any]:
|
|
366
|
+
return await self._call("list", {"category": category, "query": query})
|
|
367
|
+
|
|
368
|
+
async def counts(self, category: str = "", query: str = "") -> dict[str, Any]:
|
|
369
|
+
"""How many items match, in all and per category — exact, unlike a list,
|
|
370
|
+
which stops at 500."""
|
|
371
|
+
return await self._call("counts", {"category": category, "query": query})
|
|
372
|
+
|
|
373
|
+
async def get(self, id: str) -> dict[str, Any]:
|
|
374
|
+
return await self._call("get", {"id": id})
|
|
375
|
+
|
|
376
|
+
async def save(
|
|
377
|
+
self, title: str, category: str = "", fields: Any = None, note: str = ""
|
|
378
|
+
) -> dict[str, Any]:
|
|
379
|
+
return await self._call(
|
|
380
|
+
"save", {"title": title, "category": category, "fields": fields or [], "note": note}
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
async def update(self, id: str, **changes: Any) -> dict[str, Any]:
|
|
384
|
+
return await self._call("update", {"id": id, **changes})
|
|
385
|
+
|
|
386
|
+
async def delete(self, id: str) -> dict[str, Any]:
|
|
387
|
+
return await self._call("delete", {"id": id})
|
|
388
|
+
|
|
389
|
+
async def history(self, id: str | None = None, *, limit: int = 30) -> dict[str, Any]:
|
|
390
|
+
return await self._call("history", {"id": id, "limit": limit})
|
|
391
|
+
|
|
392
|
+
async def undo(self, change_id: str) -> dict[str, Any]:
|
|
393
|
+
return await self._call("undo", {"change_id": change_id})
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class SharesHandle:
|
|
397
|
+
"""The shares of this agent that the person it acts for takes part in, via
|
|
398
|
+
`ctx.shares`. None outside a person's principal.
|
|
399
|
+
|
|
400
|
+
- `list()` — each share: `{id, kind, title, ref, role, principal_ref,
|
|
401
|
+
unread, you, members: [{ref, name, role}]}`. Members are pseudonyms (`ref`)
|
|
402
|
+
stable for this agent; never kernel ids or addresses.
|
|
403
|
+
- `principal(share_id)` — the share's `principal_ref`: what your SQL writes
|
|
404
|
+
into `principal_id` to put a row in the share. Row-level security decides
|
|
405
|
+
whether the write is allowed (a viewer's is not); knowing it grants nothing.
|
|
406
|
+
- `activity(share_id, kind, ref="", summary=None)` — record what happened,
|
|
407
|
+
as the person this run acts for; every member sees it.
|
|
408
|
+
- `propose(kind=, title=, ref=, invite_emails=, announce=True)` — ask the
|
|
409
|
+
person's app to open its share panel with this filled in. Nothing is
|
|
410
|
+
shared until they confirm there. `announce=False` only checks it and
|
|
411
|
+
returns the proposal, for a caller that shows it itself."""
|
|
412
|
+
|
|
413
|
+
def __init__(
|
|
414
|
+
self,
|
|
415
|
+
list_fn: Callable[[], Awaitable[list[dict[str, Any]]]],
|
|
416
|
+
principal_fn: Callable[[str], Awaitable[str]],
|
|
417
|
+
activity_fn: Callable[[str, str, str, dict[str, Any]], Awaitable[int]],
|
|
418
|
+
propose_fn: Callable[[dict[str, Any], bool], Awaitable[dict[str, Any]]],
|
|
419
|
+
) -> None:
|
|
420
|
+
self._list = list_fn
|
|
421
|
+
self._principal = principal_fn
|
|
422
|
+
self._activity = activity_fn
|
|
423
|
+
self._propose = propose_fn
|
|
424
|
+
|
|
425
|
+
async def list(self) -> list[dict[str, Any]]:
|
|
426
|
+
return await self._list()
|
|
427
|
+
|
|
428
|
+
async def principal(self, share_id: str) -> str:
|
|
429
|
+
return await self._principal(share_id)
|
|
430
|
+
|
|
431
|
+
async def activity(
|
|
432
|
+
self, share_id: str, kind: str, ref: str = "", summary: dict[str, Any] | None = None
|
|
433
|
+
) -> int:
|
|
434
|
+
return await self._activity(share_id, kind, ref, summary or {})
|
|
435
|
+
|
|
436
|
+
async def propose(
|
|
437
|
+
self,
|
|
438
|
+
*,
|
|
439
|
+
kind: str = "thing",
|
|
440
|
+
title: str,
|
|
441
|
+
ref: str = "",
|
|
442
|
+
invite_emails: list[str] | None = None,
|
|
443
|
+
announce: bool = True,
|
|
444
|
+
) -> dict[str, Any]:
|
|
445
|
+
return await self._propose(
|
|
446
|
+
{"kind": kind, "title": title, "ref": ref, "invite_emails": invite_emails or []},
|
|
447
|
+
announce,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class AgentBuilderHandle:
|
|
452
|
+
"""Narrow personal-Agent composition seam exposed via `ctx.agent_builder`.
|
|
453
|
+
|
|
454
|
+
It never accepts a raw manifest or source code: it takes a constrained spec,
|
|
455
|
+
checks it against the tools and capabilities that exist, and makes an
|
|
456
|
+
ordinary prompt-only agent from it.
|
|
457
|
+
|
|
458
|
+
The person has at most one **draft**: a spec not yet published, which they
|
|
459
|
+
can try as an agent and which their own builder edits too. `draft` reads it,
|
|
460
|
+
`save_draft` replaces it, `draft_presentation` reads or replaces how it
|
|
461
|
+
looks to them.
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
def __init__(
|
|
465
|
+
self,
|
|
466
|
+
catalog_fn: Callable[[], Awaitable[dict[str, Any]]],
|
|
467
|
+
preview_fn: Callable[[dict[str, Any]], Awaitable[dict[str, Any]]],
|
|
468
|
+
publish_fn: Callable[[dict[str, Any]], Awaitable[dict[str, Any]]],
|
|
469
|
+
list_fn: Callable[[], Awaitable[list[dict[str, Any]]]],
|
|
470
|
+
archive_fn: Callable[[str], Awaitable[None]],
|
|
471
|
+
draft_fn: Callable[[], Awaitable[dict[str, Any] | None]],
|
|
472
|
+
save_draft_fn: Callable[[dict[str, Any]], Awaitable[dict[str, Any]]],
|
|
473
|
+
draft_presentation_fn: Callable[[dict[str, Any] | None], Awaitable[dict[str, Any] | None]],
|
|
474
|
+
) -> None:
|
|
475
|
+
self._catalog = catalog_fn
|
|
476
|
+
self._preview = preview_fn
|
|
477
|
+
self._publish = publish_fn
|
|
478
|
+
self._list = list_fn
|
|
479
|
+
self._archive = archive_fn
|
|
480
|
+
self._draft = draft_fn
|
|
481
|
+
self._save_draft = save_draft_fn
|
|
482
|
+
self._draft_presentation = draft_presentation_fn
|
|
483
|
+
|
|
484
|
+
async def catalog(self) -> dict[str, Any]:
|
|
485
|
+
return await self._catalog()
|
|
486
|
+
|
|
487
|
+
async def preview(self, spec: dict[str, Any]) -> dict[str, Any]:
|
|
488
|
+
return await self._preview(spec)
|
|
489
|
+
|
|
490
|
+
async def publish(self, spec: dict[str, Any]) -> dict[str, Any]:
|
|
491
|
+
return await self._publish(spec)
|
|
492
|
+
|
|
493
|
+
async def list(self) -> list[dict[str, Any]]:
|
|
494
|
+
return await self._list()
|
|
495
|
+
|
|
496
|
+
async def archive(self, agent_id: str) -> None:
|
|
497
|
+
await self._archive(agent_id)
|
|
498
|
+
|
|
499
|
+
async def draft(self) -> dict[str, Any] | None:
|
|
500
|
+
"""The person's draft: `{spec, edits, presentation}`, or None when they
|
|
501
|
+
have none. `spec` is as it was last saved, its page markup included;
|
|
502
|
+
`edits` is the id of the published agent it is an edit of, or None for a
|
|
503
|
+
new one."""
|
|
504
|
+
return await self._draft()
|
|
505
|
+
|
|
506
|
+
async def save_draft(self, spec: dict[str, Any]) -> dict[str, Any]:
|
|
507
|
+
"""Make `spec` the person's draft, replacing the one they had. Validated
|
|
508
|
+
and composed as `preview` does — and refused the same way — and returns
|
|
509
|
+
the same summary. Nothing is published."""
|
|
510
|
+
return await self._save_draft(spec)
|
|
511
|
+
|
|
512
|
+
async def draft_presentation(
|
|
513
|
+
self, value: dict[str, Any] | None = None
|
|
514
|
+
) -> dict[str, Any] | None:
|
|
515
|
+
"""How the draft looks to the person, in the distribution's own terms.
|
|
516
|
+
With `value`, replace it whole (there has to be a draft); either way
|
|
517
|
+
returns what is stored now, None for none."""
|
|
518
|
+
return await self._draft_presentation(value)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
class StandingHandle:
|
|
522
|
+
"""What this agent has arranged for the person, via `ctx.device.standing`,
|
|
523
|
+
keyed by the agent's own `item_key` — putting the same item again changes
|
|
524
|
+
nothing.
|
|
525
|
+
|
|
526
|
+
An `alert` (a reminder at a time) is kept by the kernel: every phone of the
|
|
527
|
+
person installs it and rings on its own, and the kernel pushes it when no
|
|
528
|
+
phone did. It needs no device and can be put from anywhere.
|
|
529
|
+
|
|
530
|
+
A `location`, `alarm`, `live_activity` or `glance` item is set ON THE DEVICE
|
|
531
|
+
THE PERSON IS USING and confirmed by it before `put` returns. Called from
|
|
532
|
+
anything that is not such a device — a schedule, an event, a browser — it
|
|
533
|
+
raises `ValueError` with `code` `device.no_device` or `device.unsupported`.
|
|
534
|
+
|
|
535
|
+
A mistake the caller can fix raises `ValueError`; a permission the person
|
|
536
|
+
has not given raises one too, whose `code` is `capability_needs_approval`
|
|
537
|
+
(nobody has decided) or `capability_denied`, and whose `capability` is the
|
|
538
|
+
permission to ask them for."""
|
|
539
|
+
|
|
540
|
+
def __init__(
|
|
541
|
+
self,
|
|
542
|
+
put: Callable[[dict[str, Any]], Awaitable[dict[str, Any]]],
|
|
543
|
+
delete: Callable[[str, str | None, str | None], Awaitable[dict[str, Any]]],
|
|
544
|
+
list_: Callable[[], Awaitable[dict[str, Any]]],
|
|
545
|
+
) -> None:
|
|
546
|
+
self._put = put
|
|
547
|
+
self._delete = delete
|
|
548
|
+
self._list = list_
|
|
549
|
+
|
|
550
|
+
async def put(
|
|
551
|
+
self,
|
|
552
|
+
item_key: str,
|
|
553
|
+
kind: str,
|
|
554
|
+
spec: dict[str, Any],
|
|
555
|
+
*,
|
|
556
|
+
level: str | None = None,
|
|
557
|
+
actions: list[dict[str, Any]] | None = None,
|
|
558
|
+
to: str | None = None,
|
|
559
|
+
share: str | None = None,
|
|
560
|
+
) -> dict[str, Any]:
|
|
561
|
+
"""`kind`: alert, location, alarm, live_activity or glance. Returns
|
|
562
|
+
`{ok, item_key, level, downgraded, changed}`, and for every kind but
|
|
563
|
+
`alert` also `installed` (whether the device confirmed it) with
|
|
564
|
+
`reason` when it did not. `to` (a participant ref) with `share` puts an
|
|
565
|
+
ALERT on that participant's phones instead, on their own permissions."""
|
|
566
|
+
body: dict[str, Any] = {"item_key": item_key, "kind": kind, "spec": spec}
|
|
567
|
+
if to is not None or share is not None:
|
|
568
|
+
body["to"], body["share"] = to, share
|
|
569
|
+
if level is not None:
|
|
570
|
+
body["level"] = level
|
|
571
|
+
if actions is not None:
|
|
572
|
+
body["actions"] = actions
|
|
573
|
+
return await self._put(body)
|
|
574
|
+
|
|
575
|
+
async def delete(
|
|
576
|
+
self, item_key: str, *, to: str | None = None, share: str | None = None
|
|
577
|
+
) -> dict[str, Any]:
|
|
578
|
+
"""Take the item back from every device it is on. Works from anywhere."""
|
|
579
|
+
return await self._delete(item_key, to, share)
|
|
580
|
+
|
|
581
|
+
async def list(self) -> dict[str, Any]:
|
|
582
|
+
"""`{items: [{item_key, kind, spec, level, actions, updated_at, here,
|
|
583
|
+
devices: [{device_id, state, reason?, at}]}]}`. `here` says whether the
|
|
584
|
+
item is on the device the person is using now."""
|
|
585
|
+
return await self._list()
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
class DeviceHandle:
|
|
589
|
+
"""The device the person is on, via `ctx.device`. None for a system
|
|
590
|
+
principal.
|
|
591
|
+
|
|
592
|
+
`call(op, args)` runs one operation on that device — read their calendar,
|
|
593
|
+
ask them to pick a photo, get where they are — through the kernel, which
|
|
594
|
+
checks the permission and waits up to 15 seconds. The device is the one
|
|
595
|
+
this request came from: the one the person's message was sent on, or the
|
|
596
|
+
one this agent's page is open on. The kernel reaches no other device of
|
|
597
|
+
theirs, and a request that came from none (a schedule, an event, a chat
|
|
598
|
+
channel, work handed over by another agent) reaches none.
|
|
599
|
+
It returns `{"ok": True, "result": …}` or `{"ok": False, "code", "message"}`
|
|
600
|
+
with `code` one of `device.no_device`, `device.unsupported` (this device
|
|
601
|
+
cannot), `device.unreachable`, `device.os_denied`, `device.declined`,
|
|
602
|
+
`device.not_own`, or `capability_needs_approval` / `capability_denied`. The
|
|
603
|
+
ops and their arguments are listed in the SDK reference."""
|
|
604
|
+
|
|
605
|
+
def __init__(
|
|
606
|
+
self,
|
|
607
|
+
call: Callable[[str, dict[str, Any]], Awaitable[dict[str, Any]]],
|
|
608
|
+
standing: StandingHandle,
|
|
609
|
+
) -> None:
|
|
610
|
+
self._call = call
|
|
611
|
+
self.standing = standing
|
|
612
|
+
|
|
613
|
+
async def call(self, op: str, args: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
614
|
+
return await self._call(op, args or {})
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
class ToolContext:
|
|
618
|
+
"""Injected by the kernel; the only surface a tool author should touch."""
|
|
619
|
+
|
|
620
|
+
def __init__(
|
|
621
|
+
self,
|
|
622
|
+
principal: Principal,
|
|
623
|
+
state_factory: Callable[[str], Awaitable[_StateHandle]],
|
|
624
|
+
capability: CapabilityHandle,
|
|
625
|
+
channel: ChannelHandle,
|
|
626
|
+
budget: BudgetHandle,
|
|
627
|
+
now_fn: Callable[[], datetime],
|
|
628
|
+
config: dict[str, Any],
|
|
629
|
+
principal_config: dict[str, Any],
|
|
630
|
+
ask: AskFn | None = None,
|
|
631
|
+
session: SessionHandle | None = None,
|
|
632
|
+
schedule: ScheduleHandle | None = None,
|
|
633
|
+
dispatch: DispatchHandle | None = None,
|
|
634
|
+
attachment: AttachmentFn | None = None,
|
|
635
|
+
embedding: EmbeddingHandle | None = None,
|
|
636
|
+
agent_builder: AgentBuilderHandle | None = None,
|
|
637
|
+
memory: MemoryHandle | None = None,
|
|
638
|
+
todos: TodoHandle | None = None,
|
|
639
|
+
items: ItemHandle | None = None,
|
|
640
|
+
is_admin: AdminFn | None = None,
|
|
641
|
+
workspace: Path | None = None,
|
|
642
|
+
shares: SharesHandle | None = None,
|
|
643
|
+
notify: NotifyFn | None = None,
|
|
644
|
+
device: DeviceHandle | None = None,
|
|
645
|
+
) -> None:
|
|
646
|
+
self.principal = principal
|
|
647
|
+
self._state_factory = state_factory
|
|
648
|
+
self.capability = capability
|
|
649
|
+
self.channel = channel
|
|
650
|
+
self.budget = budget
|
|
651
|
+
#: Deferred/recurring self-scheduling; None inside an autonomous fire.
|
|
652
|
+
self.schedule = schedule
|
|
653
|
+
#: None when this run may not hand work to a peer. See `DispatchHandle`.
|
|
654
|
+
self.dispatch = dispatch
|
|
655
|
+
self._now = now_fn
|
|
656
|
+
self._config = config
|
|
657
|
+
self._principal_config = principal_config
|
|
658
|
+
self._ask = ask
|
|
659
|
+
#: Ephemeral per-session scratch state (todo/goal); None outside a run.
|
|
660
|
+
self.session = session
|
|
661
|
+
#: Where this run may put files (None outside a run). The built-in file
|
|
662
|
+
#: tools (`system.fs.read`, `system.doc.inspect`) read the same
|
|
663
|
+
#: directory, so a file left here can be handed to them. Give the model
|
|
664
|
+
#: the path RELATIVE to this directory, never the absolute one. Scratch,
|
|
665
|
+
#: not storage: files untouched for a day are deleted, and anything that
|
|
666
|
+
#: must last belongs in the agent's own state.
|
|
667
|
+
self.workspace = workspace
|
|
668
|
+
self._attachment = attachment
|
|
669
|
+
self.embedding = embedding or EmbeddingHandle(None)
|
|
670
|
+
#: Conversational prompt-only agent composition. None for system
|
|
671
|
+
#: principals; tool inventory/capabilities decide who can call it.
|
|
672
|
+
self.agent_builder = agent_builder
|
|
673
|
+
#: What is kept for the person with this agent (memory, to-dos, items).
|
|
674
|
+
#: None for system principals.
|
|
675
|
+
self.memory = memory
|
|
676
|
+
self.todos = todos
|
|
677
|
+
self.items = items
|
|
678
|
+
#: The shares of this agent its person takes part in (`SharesHandle`).
|
|
679
|
+
#: None outside a person's principal (a share's, the system's).
|
|
680
|
+
self.shares = shares
|
|
681
|
+
#: The device the person is on. None for system principals.
|
|
682
|
+
self.device = device
|
|
683
|
+
self._is_admin = is_admin
|
|
684
|
+
self._notify = notify
|
|
685
|
+
|
|
686
|
+
async def notify(
|
|
687
|
+
self,
|
|
688
|
+
text: str,
|
|
689
|
+
*,
|
|
690
|
+
title: str | None = None,
|
|
691
|
+
key: str | None = None,
|
|
692
|
+
level: str | None = None,
|
|
693
|
+
actions: list[dict[str, Any]] | None = None,
|
|
694
|
+
share: str | None = None,
|
|
695
|
+
to: str | None = None,
|
|
696
|
+
category: str | None = None,
|
|
697
|
+
) -> dict[str, Any]:
|
|
698
|
+
"""Tell a person something, now, with no model turn — what a federated
|
|
699
|
+
backend does with `POST /api/agent/notify`.
|
|
700
|
+
|
|
701
|
+
To this principal's person by default; with `to` (a participant `ref`)
|
|
702
|
+
and `share` (a share this principal belongs to), to that participant,
|
|
703
|
+
through their own principal and under their own permission. `level`:
|
|
704
|
+
`passive` | `active` (default) | `time_sensitive` (lowered to `active`
|
|
705
|
+
unless the person allowed `device.notify.urgent`; the receipt then says
|
|
706
|
+
`level_requested`). `actions` `[{id, label, destructive?, foreground?,
|
|
707
|
+
input?}]` come back as `notice.action` events, so they need a
|
|
708
|
+
`provides.events` handler that hears that kind; phones, browsers and
|
|
709
|
+
the web conversation show them, chat apps get the text only. `key` makes a retry
|
|
710
|
+
within a day a no-op; `category` is what a share participant can turn
|
|
711
|
+
off.
|
|
712
|
+
|
|
713
|
+
Returns the receipt `{ok: true, status, level, …}`, or `{ok: false,
|
|
714
|
+
code, message}` when refused (for example `notify.not_allowed`: the
|
|
715
|
+
person has not allowed this agent `person.notify`). Never raises for a
|
|
716
|
+
refusal."""
|
|
717
|
+
if self._notify is None:
|
|
718
|
+
return {
|
|
719
|
+
"ok": False,
|
|
720
|
+
"code": "notify.unavailable",
|
|
721
|
+
"message": "nobody to notify from here",
|
|
722
|
+
}
|
|
723
|
+
body: dict[str, Any] = {"text": text}
|
|
724
|
+
for name, value in (
|
|
725
|
+
("title", title),
|
|
726
|
+
("key", key),
|
|
727
|
+
("level", level),
|
|
728
|
+
("actions", actions),
|
|
729
|
+
("share", share),
|
|
730
|
+
("to", to),
|
|
731
|
+
("category", category),
|
|
732
|
+
):
|
|
733
|
+
if value is not None:
|
|
734
|
+
body[name] = value
|
|
735
|
+
return await self._notify(body)
|
|
736
|
+
|
|
737
|
+
async def state(self, namespace_id: str) -> _StateHandle:
|
|
738
|
+
return await self._state_factory(namespace_id)
|
|
739
|
+
|
|
740
|
+
async def attachment(self, attachment_id: str) -> InboundAttachment | None:
|
|
741
|
+
"""Fetch one of the caller's own attachments by id (bytes + media_type +
|
|
742
|
+
filename), or None when none of the caller's attachments has that id."""
|
|
743
|
+
if self._attachment is None:
|
|
744
|
+
return None
|
|
745
|
+
return await self._attachment(attachment_id)
|
|
746
|
+
|
|
747
|
+
async def ask(
|
|
748
|
+
self,
|
|
749
|
+
question: str,
|
|
750
|
+
choices: list[str] | None = None,
|
|
751
|
+
fields: list[dict[str, Any]] | None = None,
|
|
752
|
+
) -> AskReply | None:
|
|
753
|
+
"""Ask the user something and wait for the reply over the run's
|
|
754
|
+
interactive channel.
|
|
755
|
+
|
|
756
|
+
`question` + `choices` is the universal form: one sentence and a flat list
|
|
757
|
+
of suggested answers. Every surface can render it — a chat channel, a log
|
|
758
|
+
line, a client written before any of this was structured.
|
|
759
|
+
|
|
760
|
+
`fields` is the SAME question drawn as a form, for a surface that can draw
|
|
761
|
+
one. Each entry is ``{key, label, options, multiple, placeholder}`` (the
|
|
762
|
+
same field shape as a `form` block). It must always be a faithful elaboration of
|
|
763
|
+
`question`, never extra content — a surface that cannot draw a form
|
|
764
|
+
ignores it, and the user still has to be asked the whole thing.
|
|
765
|
+
|
|
766
|
+
Returns an `AskReply`, or None when the run has no back-channel to reach
|
|
767
|
+
the user (e.g. a cron/batch fire) or the wait times out — callers must
|
|
768
|
+
handle None."""
|
|
769
|
+
if self._ask is None:
|
|
770
|
+
return None
|
|
771
|
+
return await self._ask(question, choices, fields)
|
|
772
|
+
|
|
773
|
+
async def is_admin(self) -> bool | None:
|
|
774
|
+
"""Whether the person behind this call is one of the deployment's
|
|
775
|
+
administrators.
|
|
776
|
+
|
|
777
|
+
``True`` a person, an administrator.
|
|
778
|
+
``False`` a person, not an administrator.
|
|
779
|
+
``None`` no person behind this call (a scheduled job, a system
|
|
780
|
+
principal), or the deployment cannot say.
|
|
781
|
+
|
|
782
|
+
A fact about a person, not a permission: it authorizes nothing and
|
|
783
|
+
unlocks no tool. Use it to show someone the page that matches their
|
|
784
|
+
role; every write behind that page still needs its ordinary capability.
|
|
785
|
+
|
|
786
|
+
Each call is recorded on the run, so the person can see that an agent
|
|
787
|
+
asked.
|
|
788
|
+
"""
|
|
789
|
+
if self._is_admin is None:
|
|
790
|
+
return None
|
|
791
|
+
return await self._is_admin()
|
|
792
|
+
|
|
793
|
+
def now(self) -> datetime:
|
|
794
|
+
return self._now()
|
|
795
|
+
|
|
796
|
+
def config(self, key: str, default: Any = None) -> Any:
|
|
797
|
+
return self._config.get(key, default)
|
|
798
|
+
|
|
799
|
+
def principal_config(self, key: str, default: Any = None) -> Any:
|
|
800
|
+
return self._principal_config.get(key, default)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""`@tool` decorator — attaches metadata so the kernel can find the handler.
|
|
2
|
+
|
|
3
|
+
The manifest names the handler by Python ref; this decorator only adds a marker
|
|
4
|
+
attribute. The actual schema is declared in `manifest.yaml`. We deliberately
|
|
5
|
+
don't infer the schema from Python signatures — schema is a manifest concern.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Awaitable, Callable
|
|
11
|
+
from functools import wraps
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from aipelago.sdk.context import ToolContext
|
|
15
|
+
|
|
16
|
+
ToolHandler = Callable[[ToolContext, dict[str, Any]], Awaitable[dict[str, Any] | Any]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def tool(
|
|
20
|
+
tool_id: str,
|
|
21
|
+
*,
|
|
22
|
+
side_effects: str = "read-only",
|
|
23
|
+
idempotency: str = "unsafe",
|
|
24
|
+
input_schema: dict[str, Any] | None = None,
|
|
25
|
+
reaches: str | None = None,
|
|
26
|
+
parallelizable: bool = False,
|
|
27
|
+
capability: str | None = None,
|
|
28
|
+
) -> Callable[[ToolHandler], ToolHandler]:
|
|
29
|
+
"""Mark a coroutine as the handler for a tool.
|
|
30
|
+
|
|
31
|
+
In an agent, `manifest.yaml` is the source of truth for everything about a
|
|
32
|
+
tool, so only `tool_id` matters here and the other arguments are ignored.
|
|
33
|
+
They describe the kernel's own built-in tools, which have no manifest:
|
|
34
|
+
|
|
35
|
+
* `side_effects` — `read-only` or not; it decides the sandbox a call runs in.
|
|
36
|
+
* `input_schema` — JSON Schema for the arguments, shown to the model and
|
|
37
|
+
checked before the call.
|
|
38
|
+
* `reaches` — the argument holding the host the tool talks to, when it is
|
|
39
|
+
not `url`, so the person can be asked about that host.
|
|
40
|
+
* `parallelizable` — two calls in one turn may run together (read-only
|
|
41
|
+
tools only).
|
|
42
|
+
* `capability` — the permission every call needs, decided (asking the
|
|
43
|
+
person when it is theirs to give) before the handler runs.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def deco(fn: ToolHandler) -> ToolHandler:
|
|
47
|
+
@wraps(fn)
|
|
48
|
+
async def wrapped(ctx: ToolContext, args: dict[str, Any]) -> Any:
|
|
49
|
+
return await fn(ctx, args)
|
|
50
|
+
|
|
51
|
+
wrapped.__aipelago_tool_id__ = tool_id # type: ignore[attr-defined]
|
|
52
|
+
wrapped.__aipelago_side_effects__ = side_effects # type: ignore[attr-defined]
|
|
53
|
+
wrapped.__aipelago_idempotency__ = idempotency # type: ignore[attr-defined]
|
|
54
|
+
wrapped.__aipelago_input_schema__ = input_schema # type: ignore[attr-defined]
|
|
55
|
+
wrapped.__aipelago_reaches__ = reaches # type: ignore[attr-defined]
|
|
56
|
+
wrapped.__aipelago_parallelizable__ = parallelizable # type: ignore[attr-defined]
|
|
57
|
+
wrapped.__aipelago_capability__ = capability # type: ignore[attr-defined]
|
|
58
|
+
return wrapped
|
|
59
|
+
|
|
60
|
+
return deco
|
aipelago/sdk/hook.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Lifecycle hooks: the `@hook` decorator and the `HookDecision` a handler returns.
|
|
2
|
+
|
|
3
|
+
`@hook` mirrors `@tool`: the manifest declares the hook (event + handler ref +
|
|
4
|
+
matcher); this decorator only marks the handler. A handler receives the event
|
|
5
|
+
payload dict and returns a `HookDecision` (or None = allow).
|
|
6
|
+
|
|
7
|
+
from aipelago.sdk import hook, HookDecision
|
|
8
|
+
|
|
9
|
+
@hook("pre_tool_use")
|
|
10
|
+
async def block_secrets(payload: dict) -> HookDecision | None:
|
|
11
|
+
if "secret" in str(payload.get("args", {})):
|
|
12
|
+
return HookDecision(decision="deny", reason="no secrets in args")
|
|
13
|
+
return None
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Awaitable, Callable
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from functools import wraps
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class HookDecision:
|
|
26
|
+
"""A hook's verdict. A handler returns this (or None = allow); an external
|
|
27
|
+
command prints it as JSON on stdout. Which fields count depends on the event:
|
|
28
|
+
``args`` on pre_tool_use, ``text`` on user_prompt_submit, ``output`` on
|
|
29
|
+
post_tool_use; ``steer`` is guidance the model sees, on deny too, on the
|
|
30
|
+
events that accept it. Events that only observe ignore every field."""
|
|
31
|
+
|
|
32
|
+
decision: str = "allow" # "allow" | "deny"
|
|
33
|
+
reason: str | None = None
|
|
34
|
+
args: dict[str, Any] | None = None
|
|
35
|
+
text: str | None = None
|
|
36
|
+
output: Any | None = None
|
|
37
|
+
steer: str | None = None
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def is_deny(self) -> bool:
|
|
41
|
+
return self.decision == "deny"
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def allow(cls) -> HookDecision:
|
|
45
|
+
return cls()
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_payload(cls, raw: Any) -> HookDecision:
|
|
49
|
+
"""Parse a hook's raw output (e.g. JSON from an external command) into a
|
|
50
|
+
decision. Anything that is not a dict reads as allow."""
|
|
51
|
+
if not isinstance(raw, dict):
|
|
52
|
+
return cls.allow()
|
|
53
|
+
decision = "deny" if str(raw.get("decision", "allow")).lower() == "deny" else "allow"
|
|
54
|
+
args = raw.get("args")
|
|
55
|
+
text = raw.get("text")
|
|
56
|
+
steer = raw.get("steer")
|
|
57
|
+
return cls(
|
|
58
|
+
decision=decision,
|
|
59
|
+
reason=raw.get("reason"),
|
|
60
|
+
args=args if isinstance(args, dict) else None,
|
|
61
|
+
text=str(text) if text is not None else None,
|
|
62
|
+
output=raw.get("output"),
|
|
63
|
+
steer=str(steer) if steer is not None else None,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
HookHandler = Callable[[dict[str, Any]], Awaitable["HookDecision | None"]]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def hook(event: str) -> Callable[[HookHandler], HookHandler]:
|
|
71
|
+
"""Mark a coroutine as the handler for a manifest-declared lifecycle hook."""
|
|
72
|
+
|
|
73
|
+
def deco(fn: HookHandler) -> HookHandler:
|
|
74
|
+
@wraps(fn)
|
|
75
|
+
async def wrapped(payload: dict[str, Any]) -> Any:
|
|
76
|
+
return await fn(payload)
|
|
77
|
+
|
|
78
|
+
wrapped.__aipelago_hook_event__ = event # type: ignore[attr-defined]
|
|
79
|
+
return wrapped
|
|
80
|
+
|
|
81
|
+
return deco
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""`Principal`: whom a call is made for — `(owner, agent)`, the owner a person or
|
|
2
|
+
a share. A tool handler reads it as `ctx.principal`.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
from fnmatch import fnmatchcase
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PrincipalStatus(StrEnum):
|
|
16
|
+
active = "active"
|
|
17
|
+
suspended = "suspended"
|
|
18
|
+
removed = "removed"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Principal(BaseModel):
|
|
22
|
+
"""An `(owner, agent)` row. The unit of state/capability/budget partitioning.
|
|
23
|
+
|
|
24
|
+
Exactly one of three shapes: a person's (`user_id`), a share's (`share_id` —
|
|
25
|
+
a thing or a conversation several people use together, which owns its own
|
|
26
|
+
rows), or the deployment's own identity for the agent (neither)."""
|
|
27
|
+
|
|
28
|
+
model_config = ConfigDict(frozen=True)
|
|
29
|
+
|
|
30
|
+
principal_id: UUID
|
|
31
|
+
user_id: UUID | None
|
|
32
|
+
agent_id: str
|
|
33
|
+
activated_at: datetime
|
|
34
|
+
status: PrincipalStatus
|
|
35
|
+
#: When the call came with a token limited to some of the agent's
|
|
36
|
+
#: capabilities, the patterns it is limited to; None = no such limit. It
|
|
37
|
+
#: only ever narrows what the agent was granted.
|
|
38
|
+
capability_scope: frozenset[str] | None = None
|
|
39
|
+
#: When the person arrived from your own product, signed in there, their id
|
|
40
|
+
#: in your product; tokens minted for your backend carry it as `sub`. None
|
|
41
|
+
#: otherwise.
|
|
42
|
+
native_sub: str | None = None
|
|
43
|
+
#: The share this principal is the owner-of-rows for; None for a person's
|
|
44
|
+
#: or the system's.
|
|
45
|
+
share_id: UUID | None = None
|
|
46
|
+
#: The person's device this request came from — the one their message was
|
|
47
|
+
#: sent on, or the one the agent's page is open on. The only device
|
|
48
|
+
#: `ctx.device` reaches for this request; None for anything that did not
|
|
49
|
+
#: come from one (a schedule, an event, a chat channel, handed-over work).
|
|
50
|
+
device_id: UUID | None = None
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def is_system(self) -> bool:
|
|
54
|
+
"""System principals have no owner — driven only by cron/event triggers."""
|
|
55
|
+
return self.user_id is None and self.share_id is None
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def is_share(self) -> bool:
|
|
59
|
+
"""A share's principal: its rows are the shared thing's, and whoever
|
|
60
|
+
acts on it is a participant, not its owner."""
|
|
61
|
+
return self.share_id is not None
|
|
62
|
+
|
|
63
|
+
def scope_permits(self, capability_id: str) -> bool:
|
|
64
|
+
if self.capability_scope is None:
|
|
65
|
+
return True
|
|
66
|
+
return any(fnmatchcase(capability_id, g) for g in self.capability_scope)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aipelago-sdk
|
|
3
|
+
Version: 0.6.1
|
|
4
|
+
Summary: The SDK agents are written against: @tool, @hook and the ToolContext a handler receives.
|
|
5
|
+
Author-email: Jiaan Liu <jiaanliux@gmail.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: pydantic==2.12.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# aipelago-sdk
|
|
13
|
+
|
|
14
|
+
The author-facing API for Aipelago agents: `@tool`, `@hook`, and the `ToolContext`
|
|
15
|
+
the kernel injects into every handler.
|
|
16
|
+
|
|
17
|
+
The same API on every distribution of the kernel, so an agent written for one
|
|
18
|
+
runs on all of them. A distribution's developer CLI (for Ailands, `pip install
|
|
19
|
+
ailands`) depends on this package, so installing the CLI brings it in.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from aipelago.sdk import tool, ToolContext
|
|
23
|
+
|
|
24
|
+
@tool("weather.today")
|
|
25
|
+
async def today(ctx: ToolContext, args: dict) -> dict:
|
|
26
|
+
return {"forecast": "sun"}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Installing this package needs no kernel: it exists so you can type-check and
|
|
30
|
+
unit-test an agent on your own machine.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
aipelago/sdk/__init__.py,sha256=bKTVSZLKApx9sV59DirvcGAildi3CrIVCO2-Xw88kvI,922
|
|
2
|
+
aipelago/sdk/context.py,sha256=XsJexq1wQ2YbPpirXOVNremZdcyPb9SCnNAm6IwyjFY,33003
|
|
3
|
+
aipelago/sdk/decorator.py,sha256=Jv_p1j9aPig7lM3xtareSRR9TqZcayKZXkyLNp95i_s,2562
|
|
4
|
+
aipelago/sdk/hook.py,sha256=ubZjNJroeiszXhjPvAMuzLvplfMlchOCLJEir_0Ao6w,2865
|
|
5
|
+
aipelago/sdk/principal.py,sha256=D33FwrH-oYfQxp3HlTMN3ucSh-hOgh7zrwjBW7oYZIg,2511
|
|
6
|
+
aipelago_sdk-0.6.1.dist-info/METADATA,sha256=0_2kV1LYqVonZIVnw5dFCIAfpKV9HhneOiPggsKDsqs,1003
|
|
7
|
+
aipelago_sdk-0.6.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
+
aipelago_sdk-0.6.1.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
9
|
+
aipelago_sdk-0.6.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|