kimi-agent-module-api 2.0.0__tar.gz → 2.2.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.
Files changed (24) hide show
  1. {kimi_agent_module_api-2.0.0/src/kimi_agent_module_api.egg-info → kimi_agent_module_api-2.2.0}/PKG-INFO +26 -8
  2. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0}/README.md +25 -7
  3. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0}/pyproject.toml +1 -1
  4. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api/__init__.py +5 -0
  5. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api/contracts.py +2 -0
  6. kimi_agent_module_api-2.2.0/src/kimi_agent_module_api/files.py +61 -0
  7. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api/testing.py +66 -0
  8. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api/tools.py +7 -0
  9. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0/src/kimi_agent_module_api.egg-info}/PKG-INFO +26 -8
  10. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api.egg-info/SOURCES.txt +2 -0
  11. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0}/tests/test_contracts.py +5 -0
  12. kimi_agent_module_api-2.2.0/tests/test_files.py +41 -0
  13. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0}/LICENSE +0 -0
  14. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0}/setup.cfg +0 -0
  15. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api/events.py +0 -0
  16. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api/images.py +0 -0
  17. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api/py.typed +0 -0
  18. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api/settings.py +0 -0
  19. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api/trust.py +0 -0
  20. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api.egg-info/dependency_links.txt +0 -0
  21. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api.egg-info/requires.txt +0 -0
  22. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api.egg-info/top_level.txt +0 -0
  23. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.0}/tests/test_public_api.py +0 -0
  24. {kimi_agent_module_api-2.0.0 → kimi_agent_module_api-2.2.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.0.0
3
+ Version: 2.2.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.
@@ -69,23 +71,39 @@ documents installation, declarations, lifecycle, and every runtime port. The
69
71
  [reference module](https://github.com/webhead2oo9/kimi-agent/tree/main/bot/modules/example)
70
72
  is a complete, commented example that exercises most ports; start there.
71
73
 
72
- Guild-scoped live command replacement was added in 1.1. Modules using
74
+ Modules using guild-scoped live command replacement through
73
75
  `InteractionRouter.replace_guild_commands()` should require the host capability
74
76
  `discord.guild_commands.v1`.
75
77
 
76
- Version 1.2 adds typed modal forms and a narrow Components V2 layout model. Once a response uses
78
+ The SDK provides typed modal forms and a narrow Components V2 layout model. Once a response uses
77
79
  that layout model, Discord requires every later edit of the same message to remain a layout.
78
80
  Modules using them should require
79
81
  `discord.modals.v1` and/or `discord.components_v2.v1`.
80
82
 
81
- Version 1.3 adds cached author classification to message-deletion events:
83
+ 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
- Version 2 requires an explicit, source-pinned `ModuleSpec.api_version`, and
86
- removes the temporary guild-settings legacy flag and module table aliases.
87
- Modules must use namespaced guild documents and migrate legacy tables to the
88
- physical names returned by `ctx.storage.table()` before upgrading.
87
+ SDK 2.1 adds `ModuleToolContext.trigger_discord_message_id`. Mention-path tool
88
+ calls receive the exact source Discord message snowflake; other surfaces receive
89
+ `None`. Modules that act on a user's source message should require
90
+ `kimi-agent-module-api>=2.1,<3`, reject `None`, fetch that exact message through
91
+ `ctx.discord`, and verify its author before acting.
92
+
93
+ Modules use namespaced guild documents and the physical table names returned
94
+ by `ctx.storage.table()`.
95
+
96
+ SDK 2.2 adds optional `ModuleToolContext.files`. Declare
97
+ `ModulePermissions(tool_files=True)`, depend on `kimi-agent-module-api>=2.2,<3`,
98
+ and require `tools.files.v1` in `ModuleSpec.requires_capabilities`. Existing modules
99
+ keep `api_version=2` and receive `files=None` without the permission.
100
+ `ctx.files.attachments` lists admitted current/reply entries;
101
+ `read_attachment(id, max_bytes=...)` and `read_workspace(path, max_bytes=...)`
102
+ return bounded bytes without network downloads. The reader expires when the handler
103
+ returns and is scoped to the actual caller. `testing.FakeToolFiles` supports
104
+ independent tests. See the
105
+ [file access guide](https://github.com/webhead2oo9/kimi-agent/blob/main/docs/module-files.md)
106
+ for moderation, reply-image availability, privacy, and limits.
89
107
 
90
108
  ## Testing the SDK
91
109
 
@@ -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.
@@ -52,23 +54,39 @@ documents installation, declarations, lifecycle, and every runtime port. The
52
54
  [reference module](https://github.com/webhead2oo9/kimi-agent/tree/main/bot/modules/example)
53
55
  is a complete, commented example that exercises most ports; start there.
54
56
 
55
- Guild-scoped live command replacement was added in 1.1. Modules using
57
+ Modules using guild-scoped live command replacement through
56
58
  `InteractionRouter.replace_guild_commands()` should require the host capability
57
59
  `discord.guild_commands.v1`.
58
60
 
59
- Version 1.2 adds typed modal forms and a narrow Components V2 layout model. Once a response uses
61
+ The SDK provides typed modal forms and a narrow Components V2 layout model. Once a response uses
60
62
  that layout model, Discord requires every later edit of the same message to remain a layout.
61
63
  Modules using them should require
62
64
  `discord.modals.v1` and/or `discord.components_v2.v1`.
63
65
 
64
- Version 1.3 adds cached author classification to message-deletion events:
66
+ 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
- Version 2 requires an explicit, source-pinned `ModuleSpec.api_version`, and
69
- removes the temporary guild-settings legacy flag and module table aliases.
70
- Modules must use namespaced guild documents and migrate legacy tables to the
71
- physical names returned by `ctx.storage.table()` before upgrading.
70
+ SDK 2.1 adds `ModuleToolContext.trigger_discord_message_id`. Mention-path tool
71
+ calls receive the exact source Discord message snowflake; other surfaces receive
72
+ `None`. Modules that act on a user's source message should require
73
+ `kimi-agent-module-api>=2.1,<3`, reject `None`, fetch that exact message through
74
+ `ctx.discord`, and verify its author before acting.
75
+
76
+ Modules use namespaced guild documents and the physical table names returned
77
+ by `ctx.storage.table()`.
78
+
79
+ SDK 2.2 adds optional `ModuleToolContext.files`. Declare
80
+ `ModulePermissions(tool_files=True)`, depend on `kimi-agent-module-api>=2.2,<3`,
81
+ and require `tools.files.v1` in `ModuleSpec.requires_capabilities`. Existing modules
82
+ keep `api_version=2` and receive `files=None` without the permission.
83
+ `ctx.files.attachments` lists admitted current/reply entries;
84
+ `read_attachment(id, max_bytes=...)` and `read_workspace(path, max_bytes=...)`
85
+ return bounded bytes without network downloads. The reader expires when the handler
86
+ returns and is scoped to the actual caller. `testing.FakeToolFiles` supports
87
+ independent tests. See the
88
+ [file access guide](https://github.com/webhead2oo9/kimi-agent/blob/main/docs/module-files.md)
89
+ for moderation, reply-image availability, privacy, and limits.
72
90
 
73
91
  ## Testing the SDK
74
92
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kimi-agent-module-api"
3
- version = "2.0.0"
3
+ version = "2.2.0"
4
4
  description = "Stable contracts for community-built assistant modules"
5
5
  requires-python = ">=3.14"
6
6
  authors = [{ name = "Webhead" }]
@@ -37,6 +37,7 @@ 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,
@@ -152,6 +153,7 @@ __all__ = [
152
153
  "MODULE_ENTRYPOINT_GROUP",
153
154
  "AppModule",
154
155
  "ConfigSnapshot",
156
+ "FileAccessError",
155
157
  "GuildSettingsSchema",
156
158
  "InviteSnapshot",
157
159
  "LayoutGallery",
@@ -183,6 +185,9 @@ __all__ = [
183
185
  "ServiceRequirement",
184
186
  "TextInputSpec",
185
187
  "TextInputStyle",
188
+ "ToolAttachment",
189
+ "ToolFile",
190
+ "ToolFiles",
186
191
  "TrustTier",
187
192
  "render_guild_settings",
188
193
  ]
@@ -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"]
@@ -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",
@@ -7,6 +7,7 @@ from dataclasses import dataclass, field
7
7
  from typing import Any, Protocol
8
8
 
9
9
  from kimi_agent_module_api.trust import TrustTier
10
+ from kimi_agent_module_api.files import ToolFiles
10
11
 
11
12
 
12
13
  @dataclass(frozen=True, slots=True)
@@ -29,6 +30,12 @@ class ModuleToolContext:
29
30
  trust_tier: TrustTier
30
31
  # Operator per-tool configuration from ``<CONFIG_DIR>/tools/<tool>.md``.
31
32
  tool_configs: Mapping[str, Mapping[str, Any]] = field(default_factory=dict)
33
+ # Discord message that initiated this model turn. It is absent for personal
34
+ # app commands and other surfaces that are not rooted in a Discord message.
35
+ trigger_discord_message_id: int | None = None
36
+ # Invocation-scoped read-only port. Declare permissions.tool_files and
37
+ # require tools.files.v1; never retain this port beyond the handler.
38
+ files: ToolFiles | None = None
32
39
 
33
40
 
34
41
  type ModuleToolHandler = Callable[[dict[str, Any], ModuleToolContext], Coroutine[Any, Any, str]]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kimi-agent-module-api
3
- Version: 2.0.0
3
+ Version: 2.2.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.
@@ -69,23 +71,39 @@ documents installation, declarations, lifecycle, and every runtime port. The
69
71
  [reference module](https://github.com/webhead2oo9/kimi-agent/tree/main/bot/modules/example)
70
72
  is a complete, commented example that exercises most ports; start there.
71
73
 
72
- Guild-scoped live command replacement was added in 1.1. Modules using
74
+ Modules using guild-scoped live command replacement through
73
75
  `InteractionRouter.replace_guild_commands()` should require the host capability
74
76
  `discord.guild_commands.v1`.
75
77
 
76
- Version 1.2 adds typed modal forms and a narrow Components V2 layout model. Once a response uses
78
+ The SDK provides typed modal forms and a narrow Components V2 layout model. Once a response uses
77
79
  that layout model, Discord requires every later edit of the same message to remain a layout.
78
80
  Modules using them should require
79
81
  `discord.modals.v1` and/or `discord.components_v2.v1`.
80
82
 
81
- Version 1.3 adds cached author classification to message-deletion events:
83
+ 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
- Version 2 requires an explicit, source-pinned `ModuleSpec.api_version`, and
86
- removes the temporary guild-settings legacy flag and module table aliases.
87
- Modules must use namespaced guild documents and migrate legacy tables to the
88
- physical names returned by `ctx.storage.table()` before upgrading.
87
+ SDK 2.1 adds `ModuleToolContext.trigger_discord_message_id`. Mention-path tool
88
+ calls receive the exact source Discord message snowflake; other surfaces receive
89
+ `None`. Modules that act on a user's source message should require
90
+ `kimi-agent-module-api>=2.1,<3`, reject `None`, fetch that exact message through
91
+ `ctx.discord`, and verify its author before acting.
92
+
93
+ Modules use namespaced guild documents and the physical table names returned
94
+ by `ctx.storage.table()`.
95
+
96
+ SDK 2.2 adds optional `ModuleToolContext.files`. Declare
97
+ `ModulePermissions(tool_files=True)`, depend on `kimi-agent-module-api>=2.2,<3`,
98
+ and require `tools.files.v1` in `ModuleSpec.requires_capabilities`. Existing modules
99
+ keep `api_version=2` and receive `files=None` without the permission.
100
+ `ctx.files.attachments` lists admitted current/reply entries;
101
+ `read_attachment(id, max_bytes=...)` and `read_workspace(path, max_bytes=...)`
102
+ return bounded bytes without network downloads. The reader expires when the handler
103
+ returns and is scoped to the actual caller. `testing.FakeToolFiles` supports
104
+ independent tests. See the
105
+ [file access guide](https://github.com/webhead2oo9/kimi-agent/blob/main/docs/module-files.md)
106
+ for moderation, reply-image availability, privacy, and limits.
89
107
 
90
108
  ## Testing the SDK
91
109
 
@@ -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,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from importlib.metadata import version
5
6
  import dataclasses
6
7
  from typing import Any
7
8
 
@@ -82,6 +83,10 @@ def test_spec_and_runtime_context_keep_stable_defaults() -> None:
82
83
  assert {"events", "scheduler", "storage", "discord", "interactions", "services"} <= required
83
84
 
84
85
 
86
+ def test_distribution_version_includes_tool_files_contract() -> None:
87
+ assert version("kimi-agent-module-api") == "2.2.0"
88
+
89
+
85
90
  def test_spec_requires_an_explicit_keyword_api_version() -> None:
86
91
  def create(_ctx: ModuleLoadContext) -> AppModule:
87
92
  raise AssertionError("not called")
@@ -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