kimi-agent-module-api 2.1.0__tar.gz → 2.3.0__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.
- {kimi_agent_module_api-2.1.0/src/kimi_agent_module_api.egg-info → kimi_agent_module_api-2.3.0}/PKG-INFO +24 -6
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/README.md +23 -5
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/pyproject.toml +1 -1
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api/__init__.py +9 -0
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api/contracts.py +2 -0
- kimi_agent_module_api-2.3.0/src/kimi_agent_module_api/files.py +61 -0
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api/testing.py +66 -0
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api/tools.py +39 -1
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0/src/kimi_agent_module_api.egg-info}/PKG-INFO +24 -6
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api.egg-info/SOURCES.txt +2 -0
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/tests/test_contracts.py +62 -3
- kimi_agent_module_api-2.3.0/tests/test_files.py +41 -0
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/LICENSE +0 -0
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/setup.cfg +0 -0
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api/events.py +0 -0
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api/images.py +0 -0
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api/py.typed +0 -0
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api/settings.py +0 -0
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api/trust.py +0 -0
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api.egg-info/dependency_links.txt +0 -0
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api.egg-info/requires.txt +0 -0
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api.egg-info/top_level.txt +0 -0
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/tests/test_public_api.py +0 -0
- {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/tests/test_testing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kimi-agent-module-api
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Stable contracts for community-built assistant modules
|
|
5
5
|
Author: Webhead
|
|
6
6
|
License-Expression: MIT
|
|
@@ -30,6 +30,8 @@ implementation, or module loader. It exports:
|
|
|
30
30
|
(storage, scheduler, events, Discord actions, interactions, HTTP, services,
|
|
31
31
|
trust, proposals, health) plus the validators the host runs at preflight.
|
|
32
32
|
- `kimi_agent_module_api.events`: the normalized `discord.*` event payloads.
|
|
33
|
+
- `kimi_agent_module_api.files`: `ToolFiles`, `ToolAttachment`, `ToolFile`, and
|
|
34
|
+
`FileAccessError` for invocation-scoped attachment and workspace reads.
|
|
33
35
|
- `kimi_agent_module_api.testing`: a fake for every port, `load_context()` for
|
|
34
36
|
exercising `create()`, and `MemoryStorage` (install the `testing` extra) so a
|
|
35
37
|
module can unit test itself with only this package installed.
|
|
@@ -82,15 +84,31 @@ Message-deletion events include cached author classification:
|
|
|
82
84
|
`MessageDeleteEvent.author_is_bot` and `MessageBulkDeleteEvent.bot_message_ids`.
|
|
83
85
|
The values remain unknown for messages that were absent from Discord's cache.
|
|
84
86
|
|
|
85
|
-
SDK 2.1 adds `ModuleToolContext.trigger_discord_message_id`.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
`
|
|
87
|
+
SDK 2.1 adds `ModuleToolContext.trigger_discord_message_id`. SDK 2.3 adds
|
|
88
|
+
`trigger_discord_message_snapshot`, an immutable host-owned capture of the
|
|
89
|
+
message, guild, channel, author, content, and bot status made at turn entry.
|
|
90
|
+
Mention-path tool calls receive both; personal and non-message surfaces receive
|
|
91
|
+
`None`. Modules that need authoritative evidence from the triggering message
|
|
92
|
+
should require `kimi-agent-module-api>=2.3,<3` and use the snapshot instead of
|
|
93
|
+
re-fetching mutable or deletable Discord state.
|
|
90
94
|
|
|
91
95
|
Modules use namespaced guild documents and the physical table names returned
|
|
92
96
|
by `ctx.storage.table()`.
|
|
93
97
|
|
|
98
|
+
SDK 2.2 adds optional `ModuleToolContext.files`. Declare
|
|
99
|
+
`ModulePermissions(tool_files=True)`, depend on `kimi-agent-module-api>=2.2,<3`,
|
|
100
|
+
and require `tools.files.v1` in `ModuleSpec.requires_capabilities`. Existing modules
|
|
101
|
+
keep `api_version=2` and receive `files=None` without the permission.
|
|
102
|
+
`ctx.files.attachments` lists admitted current/reply entries;
|
|
103
|
+
`read_attachment(id, max_bytes=...)` and `read_workspace(path, max_bytes=...)`
|
|
104
|
+
return bounded bytes without network downloads. The reader expires when the handler
|
|
105
|
+
returns and is scoped to the actual caller. `testing.FakeToolFiles` supports
|
|
106
|
+
independent tests. Module `ctx.http` methods apply an 8 MiB host ceiling even when
|
|
107
|
+
callers supply `max_bytes`; `download` buffers and validates the bounded response
|
|
108
|
+
before yielding chunks so connections are released on early consumer exit. See the
|
|
109
|
+
[file access guide](https://github.com/webhead2oo9/kimi-agent/blob/main/docs/module-files.md)
|
|
110
|
+
for moderation, reply-image availability, privacy, and limits.
|
|
111
|
+
|
|
94
112
|
## Testing the SDK
|
|
95
113
|
|
|
96
114
|
From this package directory, run its tests without installing the Kimi application:
|
|
@@ -13,6 +13,8 @@ implementation, or module loader. It exports:
|
|
|
13
13
|
(storage, scheduler, events, Discord actions, interactions, HTTP, services,
|
|
14
14
|
trust, proposals, health) plus the validators the host runs at preflight.
|
|
15
15
|
- `kimi_agent_module_api.events`: the normalized `discord.*` event payloads.
|
|
16
|
+
- `kimi_agent_module_api.files`: `ToolFiles`, `ToolAttachment`, `ToolFile`, and
|
|
17
|
+
`FileAccessError` for invocation-scoped attachment and workspace reads.
|
|
16
18
|
- `kimi_agent_module_api.testing`: a fake for every port, `load_context()` for
|
|
17
19
|
exercising `create()`, and `MemoryStorage` (install the `testing` extra) so a
|
|
18
20
|
module can unit test itself with only this package installed.
|
|
@@ -65,15 +67,31 @@ Message-deletion events include cached author classification:
|
|
|
65
67
|
`MessageDeleteEvent.author_is_bot` and `MessageBulkDeleteEvent.bot_message_ids`.
|
|
66
68
|
The values remain unknown for messages that were absent from Discord's cache.
|
|
67
69
|
|
|
68
|
-
SDK 2.1 adds `ModuleToolContext.trigger_discord_message_id`.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
`
|
|
70
|
+
SDK 2.1 adds `ModuleToolContext.trigger_discord_message_id`. SDK 2.3 adds
|
|
71
|
+
`trigger_discord_message_snapshot`, an immutable host-owned capture of the
|
|
72
|
+
message, guild, channel, author, content, and bot status made at turn entry.
|
|
73
|
+
Mention-path tool calls receive both; personal and non-message surfaces receive
|
|
74
|
+
`None`. Modules that need authoritative evidence from the triggering message
|
|
75
|
+
should require `kimi-agent-module-api>=2.3,<3` and use the snapshot instead of
|
|
76
|
+
re-fetching mutable or deletable Discord state.
|
|
73
77
|
|
|
74
78
|
Modules use namespaced guild documents and the physical table names returned
|
|
75
79
|
by `ctx.storage.table()`.
|
|
76
80
|
|
|
81
|
+
SDK 2.2 adds optional `ModuleToolContext.files`. Declare
|
|
82
|
+
`ModulePermissions(tool_files=True)`, depend on `kimi-agent-module-api>=2.2,<3`,
|
|
83
|
+
and require `tools.files.v1` in `ModuleSpec.requires_capabilities`. Existing modules
|
|
84
|
+
keep `api_version=2` and receive `files=None` without the permission.
|
|
85
|
+
`ctx.files.attachments` lists admitted current/reply entries;
|
|
86
|
+
`read_attachment(id, max_bytes=...)` and `read_workspace(path, max_bytes=...)`
|
|
87
|
+
return bounded bytes without network downloads. The reader expires when the handler
|
|
88
|
+
returns and is scoped to the actual caller. `testing.FakeToolFiles` supports
|
|
89
|
+
independent tests. Module `ctx.http` methods apply an 8 MiB host ceiling even when
|
|
90
|
+
callers supply `max_bytes`; `download` buffers and validates the bounded response
|
|
91
|
+
before yielding chunks so connections are released on early consumer exit. See the
|
|
92
|
+
[file access guide](https://github.com/webhead2oo9/kimi-agent/blob/main/docs/module-files.md)
|
|
93
|
+
for moderation, reply-image availability, privacy, and limits.
|
|
94
|
+
|
|
77
95
|
## Testing the SDK
|
|
78
96
|
|
|
79
97
|
From this package directory, run its tests without installing the Kimi application:
|
{kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api/__init__.py
RENAMED
|
@@ -37,10 +37,13 @@ from kimi_agent_module_api.contracts import (
|
|
|
37
37
|
TextInputStyle,
|
|
38
38
|
)
|
|
39
39
|
from kimi_agent_module_api.settings import ModuleSetting, ModuleSettingsDefinition
|
|
40
|
+
from kimi_agent_module_api.files import FileAccessError, ToolAttachment, ToolFile, ToolFiles
|
|
40
41
|
from kimi_agent_module_api.tools import (
|
|
41
42
|
ModuleToolContext,
|
|
42
43
|
ModuleToolHandler,
|
|
43
44
|
ModuleToolRegistry,
|
|
45
|
+
ModuleTurnBudget,
|
|
46
|
+
TriggeringDiscordMessageSnapshot,
|
|
44
47
|
)
|
|
45
48
|
from kimi_agent_module_api.trust import TrustTier
|
|
46
49
|
|
|
@@ -152,6 +155,7 @@ __all__ = [
|
|
|
152
155
|
"MODULE_ENTRYPOINT_GROUP",
|
|
153
156
|
"AppModule",
|
|
154
157
|
"ConfigSnapshot",
|
|
158
|
+
"FileAccessError",
|
|
155
159
|
"GuildSettingsSchema",
|
|
156
160
|
"InviteSnapshot",
|
|
157
161
|
"LayoutGallery",
|
|
@@ -171,6 +175,7 @@ __all__ = [
|
|
|
171
175
|
"ModuleToolContext",
|
|
172
176
|
"ModuleToolHandler",
|
|
173
177
|
"ModuleToolRegistry",
|
|
178
|
+
"ModuleTurnBudget",
|
|
174
179
|
"OutgoingLayout",
|
|
175
180
|
"ProposalActor",
|
|
176
181
|
"ProposalError",
|
|
@@ -183,6 +188,10 @@ __all__ = [
|
|
|
183
188
|
"ServiceRequirement",
|
|
184
189
|
"TextInputSpec",
|
|
185
190
|
"TextInputStyle",
|
|
191
|
+
"ToolAttachment",
|
|
192
|
+
"ToolFile",
|
|
193
|
+
"ToolFiles",
|
|
194
|
+
"TriggeringDiscordMessageSnapshot",
|
|
186
195
|
"TrustTier",
|
|
187
196
|
"render_guild_settings",
|
|
188
197
|
]
|
{kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api/contracts.py
RENAMED
|
@@ -143,6 +143,8 @@ class ModulePermissions:
|
|
|
143
143
|
override_target_policy: bool = False
|
|
144
144
|
raw_bot: bool = False
|
|
145
145
|
raw_storage: bool = False
|
|
146
|
+
# Read admitted attachments and caller-owned workspace files in tool handlers.
|
|
147
|
+
tool_files: bool = False
|
|
146
148
|
|
|
147
149
|
|
|
148
150
|
@dataclass(frozen=True, slots=True)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Read-only, caller-scoped file access during one module tool invocation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Literal, Protocol
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FileAccessError(ValueError):
|
|
10
|
+
"""A bounded, safe-to-display file error; never includes host paths or URLs."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, code: str, message: str) -> None:
|
|
13
|
+
super().__init__(message)
|
|
14
|
+
self.code = code
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class ToolAttachment:
|
|
19
|
+
"""An admitted attachment or an explicit unavailable entry.
|
|
20
|
+
|
|
21
|
+
``id`` is opaque and valid only for this invocation. ``workspace_path`` is
|
|
22
|
+
relative to the caller's workspace, and may be reused in later turns.
|
|
23
|
+
Reply images have generated filenames when the host has no source name.
|
|
24
|
+
No Discord SDK objects, signed URLs, or absolute paths are exposed.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
id: str
|
|
28
|
+
filename: str
|
|
29
|
+
size: int
|
|
30
|
+
media_type: str | None
|
|
31
|
+
source: Literal["current", "reply"] = "current"
|
|
32
|
+
workspace_path: str | None = None
|
|
33
|
+
unavailable_reason: str | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class ToolFile:
|
|
38
|
+
filename: str
|
|
39
|
+
media_type: str | None
|
|
40
|
+
data: bytes = field(repr=False)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ToolFiles(Protocol):
|
|
44
|
+
"""Available only when ``permissions.tool_files`` is declared.
|
|
45
|
+
|
|
46
|
+
Reads are bounded by the requested limit and host limits, and share an
|
|
47
|
+
aggregate byte budget per invocation. They use the caller's workspace and
|
|
48
|
+
admitted turn inputs; they never fetch arbitrary URLs or re-fetch Discord.
|
|
49
|
+
The port expires when the handler returns. Copying bytes does not extend
|
|
50
|
+
the host's privacy/retention guarantees; modules own any further persistence.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def attachments(self) -> tuple[ToolAttachment, ...]: ...
|
|
55
|
+
|
|
56
|
+
async def read_attachment(self, attachment_id: str, *, max_bytes: int) -> ToolFile: ...
|
|
57
|
+
|
|
58
|
+
async def read_workspace(self, path: str, *, max_bytes: int) -> ToolFile: ...
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
__all__ = ["FileAccessError", "ToolAttachment", "ToolFile", "ToolFiles"]
|
{kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api/testing.py
RENAMED
|
@@ -23,6 +23,7 @@ from typing import Any, TypeVar, overload
|
|
|
23
23
|
from pydantic_settings import BaseSettings
|
|
24
24
|
|
|
25
25
|
from kimi_agent_module_api.trust import TrustTier
|
|
26
|
+
from kimi_agent_module_api.files import FileAccessError, ToolAttachment, ToolFile
|
|
26
27
|
|
|
27
28
|
from kimi_agent_module_api.contracts import (
|
|
28
29
|
ALL_DISCORD_ACTIONS,
|
|
@@ -1313,6 +1314,70 @@ def load_context(
|
|
|
1313
1314
|
return context, recorder
|
|
1314
1315
|
|
|
1315
1316
|
|
|
1317
|
+
class FakeToolFiles:
|
|
1318
|
+
"""In-memory invocation files; paths and IDs are exact fixture keys.
|
|
1319
|
+
|
|
1320
|
+
``close()`` simulates handler return. Bounds and aggregate accounting match
|
|
1321
|
+
the host. This fake never reads the test process's filesystem or network.
|
|
1322
|
+
"""
|
|
1323
|
+
|
|
1324
|
+
def __init__(
|
|
1325
|
+
self,
|
|
1326
|
+
attachments: tuple[ToolAttachment, ...] = (),
|
|
1327
|
+
*,
|
|
1328
|
+
attachment_files: Mapping[str, ToolFile] | None = None,
|
|
1329
|
+
workspace_files: Mapping[str, ToolFile] | None = None,
|
|
1330
|
+
max_bytes: int = 50 * 1024 * 1024,
|
|
1331
|
+
) -> None:
|
|
1332
|
+
self._attachments = attachments
|
|
1333
|
+
self.attachment_files = dict(attachment_files or {})
|
|
1334
|
+
self.workspace_files = dict(workspace_files or {})
|
|
1335
|
+
self.reads: list[tuple[str, str, int]] = []
|
|
1336
|
+
self._remaining = min(max_bytes, 50 * 1024 * 1024)
|
|
1337
|
+
self._active = True
|
|
1338
|
+
|
|
1339
|
+
def close(self) -> None:
|
|
1340
|
+
self._active = False
|
|
1341
|
+
|
|
1342
|
+
def _check_active(self) -> None:
|
|
1343
|
+
if not self._active:
|
|
1344
|
+
raise FileAccessError("expired", "File access expired when the tool invocation ended.")
|
|
1345
|
+
|
|
1346
|
+
@property
|
|
1347
|
+
def attachments(self) -> tuple[ToolAttachment, ...]:
|
|
1348
|
+
self._check_active()
|
|
1349
|
+
return self._attachments
|
|
1350
|
+
|
|
1351
|
+
def _read(self, file: ToolFile | None, max_bytes: int) -> ToolFile:
|
|
1352
|
+
self._check_active()
|
|
1353
|
+
if type(max_bytes) is not int or max_bytes < 1:
|
|
1354
|
+
raise FileAccessError("invalid_limit", "max_bytes must be a positive integer.")
|
|
1355
|
+
if self._remaining <= 0:
|
|
1356
|
+
raise FileAccessError(
|
|
1357
|
+
"budget_exhausted", "The invocation's file read budget is exhausted."
|
|
1358
|
+
)
|
|
1359
|
+
if file is None:
|
|
1360
|
+
raise FileAccessError("unavailable", "File is not available in this fixture.")
|
|
1361
|
+
if len(file.data) > min(max_bytes, self._remaining):
|
|
1362
|
+
raise FileAccessError("too_large", "File exceeds the file read limit.")
|
|
1363
|
+
self._remaining -= len(file.data)
|
|
1364
|
+
return file
|
|
1365
|
+
|
|
1366
|
+
async def read_attachment(self, attachment_id: str, *, max_bytes: int) -> ToolFile:
|
|
1367
|
+
self._check_active()
|
|
1368
|
+
entry = next((item for item in self.attachments if item.id == attachment_id), None)
|
|
1369
|
+
if entry is None:
|
|
1370
|
+
raise FileAccessError("unknown_attachment", "Unknown attachment for this invocation.")
|
|
1371
|
+
if entry.unavailable_reason:
|
|
1372
|
+
raise FileAccessError("unavailable", entry.unavailable_reason)
|
|
1373
|
+
self.reads.append(("attachment", attachment_id, max_bytes))
|
|
1374
|
+
return self._read(self.attachment_files.get(attachment_id), max_bytes)
|
|
1375
|
+
|
|
1376
|
+
async def read_workspace(self, path: str, *, max_bytes: int) -> ToolFile:
|
|
1377
|
+
self.reads.append(("workspace", path, max_bytes))
|
|
1378
|
+
return self._read(self.workspace_files.get(path), max_bytes)
|
|
1379
|
+
|
|
1380
|
+
|
|
1316
1381
|
__all__ = [
|
|
1317
1382
|
"DiscordCall",
|
|
1318
1383
|
"FakeDiscordActions",
|
|
@@ -1327,6 +1392,7 @@ __all__ = [
|
|
|
1327
1392
|
"FakeResponse",
|
|
1328
1393
|
"FakeScheduler",
|
|
1329
1394
|
"FakeServiceRegistry",
|
|
1395
|
+
"FakeToolFiles",
|
|
1330
1396
|
"FakeTrust",
|
|
1331
1397
|
"LoadContextRecorder",
|
|
1332
1398
|
"MemoryStorage",
|
{kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api/tools.py
RENAMED
|
@@ -6,9 +6,31 @@ from collections.abc import Callable, Coroutine, Mapping
|
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
7
|
from typing import Any, Protocol
|
|
8
8
|
|
|
9
|
+
from kimi_agent_module_api.files import ToolFiles
|
|
9
10
|
from kimi_agent_module_api.trust import TrustTier
|
|
10
11
|
|
|
11
12
|
|
|
13
|
+
class ModuleTurnBudget(Protocol):
|
|
14
|
+
"""Host-owned counters shared by every module call in one outer model turn."""
|
|
15
|
+
|
|
16
|
+
def consume(self, name: str, limit: int) -> bool:
|
|
17
|
+
"""Consume one named allowance, or return ``False`` without changing it."""
|
|
18
|
+
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class TriggeringDiscordMessageSnapshot:
|
|
24
|
+
"""Immutable evidence captured from the Discord message at turn entry."""
|
|
25
|
+
|
|
26
|
+
message_id: int
|
|
27
|
+
guild_id: int
|
|
28
|
+
channel_id: int
|
|
29
|
+
author_id: int
|
|
30
|
+
content: str
|
|
31
|
+
author_is_bot: bool
|
|
32
|
+
|
|
33
|
+
|
|
12
34
|
@dataclass(frozen=True, slots=True)
|
|
13
35
|
class ModuleToolContext:
|
|
14
36
|
"""Who is calling a module tool, and from where.
|
|
@@ -32,6 +54,16 @@ class ModuleToolContext:
|
|
|
32
54
|
# Discord message that initiated this model turn. It is absent for personal
|
|
33
55
|
# app commands and other surfaces that are not rooted in a Discord message.
|
|
34
56
|
trigger_discord_message_id: int | None = None
|
|
57
|
+
# Invocation-scoped read-only port. Declare permissions.tool_files and
|
|
58
|
+
# require tools.files.v1; never retain this port beyond the handler.
|
|
59
|
+
files: ToolFiles | None = None
|
|
60
|
+
# Host-owned values captured before turn preparation can await. Unlike a
|
|
61
|
+
# later Discord fetch, this evidence cannot change or disappear mid-turn.
|
|
62
|
+
# Added after all API 2.2 fields to preserve positional construction.
|
|
63
|
+
trigger_discord_message_snapshot: TriggeringDiscordMessageSnapshot | None = None
|
|
64
|
+
# Mutable host-owned port whose lifetime is the complete outer model turn.
|
|
65
|
+
# Counters are automatically namespaced to the installed module.
|
|
66
|
+
turn_budget: ModuleTurnBudget | None = None
|
|
35
67
|
|
|
36
68
|
|
|
37
69
|
type ModuleToolHandler = Callable[[dict[str, Any], ModuleToolContext], Coroutine[Any, Any, str]]
|
|
@@ -67,4 +99,10 @@ class ModuleToolRegistry(Protocol):
|
|
|
67
99
|
) -> None: ...
|
|
68
100
|
|
|
69
101
|
|
|
70
|
-
__all__ = [
|
|
102
|
+
__all__ = [
|
|
103
|
+
"ModuleToolContext",
|
|
104
|
+
"ModuleToolHandler",
|
|
105
|
+
"ModuleToolRegistry",
|
|
106
|
+
"ModuleTurnBudget",
|
|
107
|
+
"TriggeringDiscordMessageSnapshot",
|
|
108
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kimi-agent-module-api
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Stable contracts for community-built assistant modules
|
|
5
5
|
Author: Webhead
|
|
6
6
|
License-Expression: MIT
|
|
@@ -30,6 +30,8 @@ implementation, or module loader. It exports:
|
|
|
30
30
|
(storage, scheduler, events, Discord actions, interactions, HTTP, services,
|
|
31
31
|
trust, proposals, health) plus the validators the host runs at preflight.
|
|
32
32
|
- `kimi_agent_module_api.events`: the normalized `discord.*` event payloads.
|
|
33
|
+
- `kimi_agent_module_api.files`: `ToolFiles`, `ToolAttachment`, `ToolFile`, and
|
|
34
|
+
`FileAccessError` for invocation-scoped attachment and workspace reads.
|
|
33
35
|
- `kimi_agent_module_api.testing`: a fake for every port, `load_context()` for
|
|
34
36
|
exercising `create()`, and `MemoryStorage` (install the `testing` extra) so a
|
|
35
37
|
module can unit test itself with only this package installed.
|
|
@@ -82,15 +84,31 @@ Message-deletion events include cached author classification:
|
|
|
82
84
|
`MessageDeleteEvent.author_is_bot` and `MessageBulkDeleteEvent.bot_message_ids`.
|
|
83
85
|
The values remain unknown for messages that were absent from Discord's cache.
|
|
84
86
|
|
|
85
|
-
SDK 2.1 adds `ModuleToolContext.trigger_discord_message_id`.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
`
|
|
87
|
+
SDK 2.1 adds `ModuleToolContext.trigger_discord_message_id`. SDK 2.3 adds
|
|
88
|
+
`trigger_discord_message_snapshot`, an immutable host-owned capture of the
|
|
89
|
+
message, guild, channel, author, content, and bot status made at turn entry.
|
|
90
|
+
Mention-path tool calls receive both; personal and non-message surfaces receive
|
|
91
|
+
`None`. Modules that need authoritative evidence from the triggering message
|
|
92
|
+
should require `kimi-agent-module-api>=2.3,<3` and use the snapshot instead of
|
|
93
|
+
re-fetching mutable or deletable Discord state.
|
|
90
94
|
|
|
91
95
|
Modules use namespaced guild documents and the physical table names returned
|
|
92
96
|
by `ctx.storage.table()`.
|
|
93
97
|
|
|
98
|
+
SDK 2.2 adds optional `ModuleToolContext.files`. Declare
|
|
99
|
+
`ModulePermissions(tool_files=True)`, depend on `kimi-agent-module-api>=2.2,<3`,
|
|
100
|
+
and require `tools.files.v1` in `ModuleSpec.requires_capabilities`. Existing modules
|
|
101
|
+
keep `api_version=2` and receive `files=None` without the permission.
|
|
102
|
+
`ctx.files.attachments` lists admitted current/reply entries;
|
|
103
|
+
`read_attachment(id, max_bytes=...)` and `read_workspace(path, max_bytes=...)`
|
|
104
|
+
return bounded bytes without network downloads. The reader expires when the handler
|
|
105
|
+
returns and is scoped to the actual caller. `testing.FakeToolFiles` supports
|
|
106
|
+
independent tests. Module `ctx.http` methods apply an 8 MiB host ceiling even when
|
|
107
|
+
callers supply `max_bytes`; `download` buffers and validates the bounded response
|
|
108
|
+
before yielding chunks so connections are released on early consumer exit. See the
|
|
109
|
+
[file access guide](https://github.com/webhead2oo9/kimi-agent/blob/main/docs/module-files.md)
|
|
110
|
+
for moderation, reply-image availability, privacy, and limits.
|
|
111
|
+
|
|
94
112
|
## Testing the SDK
|
|
95
113
|
|
|
96
114
|
From this package directory, run its tests without installing the Kimi application:
|
|
@@ -4,6 +4,7 @@ pyproject.toml
|
|
|
4
4
|
src/kimi_agent_module_api/__init__.py
|
|
5
5
|
src/kimi_agent_module_api/contracts.py
|
|
6
6
|
src/kimi_agent_module_api/events.py
|
|
7
|
+
src/kimi_agent_module_api/files.py
|
|
7
8
|
src/kimi_agent_module_api/images.py
|
|
8
9
|
src/kimi_agent_module_api/py.typed
|
|
9
10
|
src/kimi_agent_module_api/settings.py
|
|
@@ -16,5 +17,6 @@ src/kimi_agent_module_api.egg-info/dependency_links.txt
|
|
|
16
17
|
src/kimi_agent_module_api.egg-info/requires.txt
|
|
17
18
|
src/kimi_agent_module_api.egg-info/top_level.txt
|
|
18
19
|
tests/test_contracts.py
|
|
20
|
+
tests/test_files.py
|
|
19
21
|
tests/test_public_api.py
|
|
20
22
|
tests/test_testing.py
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from importlib.metadata import version
|
|
6
5
|
import dataclasses
|
|
6
|
+
from importlib.metadata import version
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
import pytest
|
|
@@ -19,6 +19,7 @@ from kimi_agent_module_api import (
|
|
|
19
19
|
ModuleRuntimeContext,
|
|
20
20
|
ModuleSpec,
|
|
21
21
|
ModuleToolContext,
|
|
22
|
+
TriggeringDiscordMessageSnapshot,
|
|
22
23
|
TrustTier,
|
|
23
24
|
render_guild_settings,
|
|
24
25
|
)
|
|
@@ -83,8 +84,66 @@ def test_spec_and_runtime_context_keep_stable_defaults() -> None:
|
|
|
83
84
|
assert {"events", "scheduler", "storage", "discord", "interactions", "services"} <= required
|
|
84
85
|
|
|
85
86
|
|
|
86
|
-
def
|
|
87
|
-
assert version("kimi-agent-module-api") == "2.
|
|
87
|
+
def test_distribution_version_includes_triggering_message_snapshot_contract() -> None:
|
|
88
|
+
assert version("kimi-agent-module-api") == "2.3.0"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_triggering_discord_message_snapshot_is_immutable() -> None:
|
|
92
|
+
snapshot = TriggeringDiscordMessageSnapshot(
|
|
93
|
+
message_id=11,
|
|
94
|
+
guild_id=22,
|
|
95
|
+
channel_id=33,
|
|
96
|
+
author_id=44,
|
|
97
|
+
content="original",
|
|
98
|
+
author_is_bot=False,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
with pytest.raises(dataclasses.FrozenInstanceError):
|
|
102
|
+
snapshot.content = "edited" # type: ignore[misc]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_module_tool_context_preserves_api_2_2_nine_positional_arguments() -> None:
|
|
106
|
+
files = object()
|
|
107
|
+
|
|
108
|
+
context = ModuleToolContext(
|
|
109
|
+
11,
|
|
110
|
+
"Alice",
|
|
111
|
+
22,
|
|
112
|
+
33,
|
|
113
|
+
44,
|
|
114
|
+
TrustTier.REGULAR,
|
|
115
|
+
{"demo": {"enabled": True}},
|
|
116
|
+
55,
|
|
117
|
+
files, # type: ignore[arg-type]
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
assert context.files is files
|
|
121
|
+
assert context.trigger_discord_message_snapshot is None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_module_tool_context_adds_turn_budget_after_prior_positional_fields() -> None:
|
|
125
|
+
class Budget:
|
|
126
|
+
def consume(self, name: str, limit: int) -> bool:
|
|
127
|
+
return bool(name) and limit > 0
|
|
128
|
+
|
|
129
|
+
snapshot = TriggeringDiscordMessageSnapshot(55, 22, 33, 11, "content", False)
|
|
130
|
+
budget = Budget()
|
|
131
|
+
context = ModuleToolContext(
|
|
132
|
+
11,
|
|
133
|
+
"Alice",
|
|
134
|
+
22,
|
|
135
|
+
33,
|
|
136
|
+
44,
|
|
137
|
+
TrustTier.REGULAR,
|
|
138
|
+
{},
|
|
139
|
+
55,
|
|
140
|
+
None,
|
|
141
|
+
snapshot,
|
|
142
|
+
budget,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
assert context.trigger_discord_message_snapshot is snapshot
|
|
146
|
+
assert context.turn_budget is budget
|
|
88
147
|
|
|
89
148
|
|
|
90
149
|
def test_spec_requires_an_explicit_keyword_api_version() -> None:
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from kimi_agent_module_api import FileAccessError, ToolAttachment, ToolFile, ToolFiles
|
|
6
|
+
from kimi_agent_module_api.testing import FakeToolFiles
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.asyncio
|
|
10
|
+
async def test_fake_preserves_bytes_and_enforces_aggregate_budget() -> None:
|
|
11
|
+
attachment = ToolAttachment("a", "image.png", 3, "image/png")
|
|
12
|
+
file = ToolFile("image.png", "image/png", b"raw")
|
|
13
|
+
fake = FakeToolFiles(
|
|
14
|
+
(attachment,),
|
|
15
|
+
attachment_files={"a": file},
|
|
16
|
+
workspace_files={"saved.png": file},
|
|
17
|
+
max_bytes=5,
|
|
18
|
+
)
|
|
19
|
+
port: ToolFiles = fake
|
|
20
|
+
assert (await port.read_attachment("a", max_bytes=3)).data == b"raw"
|
|
21
|
+
with pytest.raises(FileAccessError, match="limit"):
|
|
22
|
+
await port.read_workspace("saved.png", max_bytes=3)
|
|
23
|
+
assert "raw" not in repr(file)
|
|
24
|
+
fake.close()
|
|
25
|
+
with pytest.raises(FileAccessError) as error:
|
|
26
|
+
await port.read_attachment("a", max_bytes=3)
|
|
27
|
+
assert error.value.code == "expired"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.mark.asyncio
|
|
31
|
+
async def test_fake_does_not_read_unavailable_attachments() -> None:
|
|
32
|
+
fake = FakeToolFiles(
|
|
33
|
+
(ToolAttachment("a", "audio.wav", 3, "audio/wav", unavailable_reason="Blocked"),),
|
|
34
|
+
attachment_files={"a": ToolFile("audio.wav", "audio/wav", b"raw")},
|
|
35
|
+
)
|
|
36
|
+
with pytest.raises(FileAccessError, match="Blocked"):
|
|
37
|
+
await fake.read_attachment("a", max_bytes=3)
|
|
38
|
+
with pytest.raises(FileAccessError) as error:
|
|
39
|
+
await fake.read_attachment("other", max_bytes=3)
|
|
40
|
+
assert error.value.code == "unknown_attachment"
|
|
41
|
+
assert not fake.reads
|
|
File without changes
|
|
File without changes
|
{kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api/events.py
RENAMED
|
File without changes
|
{kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api/images.py
RENAMED
|
File without changes
|
{kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api/py.typed
RENAMED
|
File without changes
|
{kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api/settings.py
RENAMED
|
File without changes
|
{kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.3.0}/src/kimi_agent_module_api/trust.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|