kimi-agent-module-api 1.2.0__tar.gz → 2.0.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-1.2.0/src/kimi_agent_module_api.egg-info → kimi_agent_module_api-2.0.0}/PKG-INFO +40 -5
- {kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/README.md +39 -4
- {kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/pyproject.toml +8 -1
- {kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/src/kimi_agent_module_api/__init__.py +17 -24
- {kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/src/kimi_agent_module_api/contracts.py +211 -1
- {kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/src/kimi_agent_module_api/events.py +2 -0
- {kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/src/kimi_agent_module_api/testing.py +216 -4
- {kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/src/kimi_agent_module_api/tools.py +4 -0
- {kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0/src/kimi_agent_module_api.egg-info}/PKG-INFO +40 -5
- {kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/src/kimi_agent_module_api.egg-info/SOURCES.txt +4 -1
- kimi_agent_module_api-2.0.0/tests/test_contracts.py +203 -0
- kimi_agent_module_api-2.0.0/tests/test_public_api.py +59 -0
- kimi_agent_module_api-2.0.0/tests/test_testing.py +114 -0
- {kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/LICENSE +0 -0
- {kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/setup.cfg +0 -0
- {kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/src/kimi_agent_module_api/images.py +0 -0
- {kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/src/kimi_agent_module_api/py.typed +0 -0
- {kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/src/kimi_agent_module_api/settings.py +0 -0
- {kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/src/kimi_agent_module_api/trust.py +0 -0
- {kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/src/kimi_agent_module_api.egg-info/dependency_links.txt +0 -0
- {kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/src/kimi_agent_module_api.egg-info/requires.txt +0 -0
- {kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/src/kimi_agent_module_api.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kimi-agent-module-api
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Stable contracts for community-built assistant modules
|
|
5
5
|
Author: Webhead
|
|
6
6
|
License-Expression: MIT
|
|
@@ -39,23 +39,58 @@ group:
|
|
|
39
39
|
|
|
40
40
|
```toml
|
|
41
41
|
[project]
|
|
42
|
-
dependencies = ["kimi-agent-module-api>=
|
|
42
|
+
dependencies = ["kimi-agent-module-api>=2,<3"]
|
|
43
43
|
|
|
44
44
|
[project.entry-points."kimi_agent.modules"]
|
|
45
45
|
my_module = "my_module_package:SPEC"
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
The source must pin the API contract it implements when constructing the
|
|
49
|
+
specification:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from kimi_agent_module_api import ModuleSpec
|
|
53
|
+
|
|
54
|
+
SPEC = ModuleSpec(
|
|
55
|
+
name="my_module",
|
|
56
|
+
version="0.1.0",
|
|
57
|
+
create=create,
|
|
58
|
+
api_version=2,
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`api_version` is a required keyword. Keep it as a literal rather than deriving
|
|
63
|
+
it from the installed SDK's `MODULE_API_VERSION`; unchanged module source must
|
|
64
|
+
not silently claim compatibility merely because it was rebuilt with a newer
|
|
65
|
+
SDK.
|
|
66
|
+
|
|
48
67
|
The [module guide](https://github.com/webhead2oo9/kimi-agent/blob/main/docs/modules.md)
|
|
49
68
|
documents installation, declarations, lifecycle, and every runtime port. The
|
|
50
69
|
[reference module](https://github.com/webhead2oo9/kimi-agent/tree/main/bot/modules/example)
|
|
51
70
|
is a complete, commented example that exercises most ports; start there.
|
|
52
71
|
|
|
53
72
|
Guild-scoped live command replacement was added in 1.1. Modules using
|
|
54
|
-
`InteractionRouter.replace_guild_commands()` should
|
|
55
|
-
`kimi-agent-module-api>=1.1,<2` and require the host capability
|
|
73
|
+
`InteractionRouter.replace_guild_commands()` should require the host capability
|
|
56
74
|
`discord.guild_commands.v1`.
|
|
57
75
|
|
|
58
76
|
Version 1.2 adds typed modal forms and a narrow Components V2 layout model. Once a response uses
|
|
59
77
|
that layout model, Discord requires every later edit of the same message to remain a layout.
|
|
60
|
-
Modules using them should
|
|
78
|
+
Modules using them should require
|
|
61
79
|
`discord.modals.v1` and/or `discord.components_v2.v1`.
|
|
80
|
+
|
|
81
|
+
Version 1.3 adds cached author classification to message-deletion events:
|
|
82
|
+
`MessageDeleteEvent.author_is_bot` and `MessageBulkDeleteEvent.bot_message_ids`.
|
|
83
|
+
The values remain unknown for messages that were absent from Discord's cache.
|
|
84
|
+
|
|
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.
|
|
89
|
+
|
|
90
|
+
## Testing the SDK
|
|
91
|
+
|
|
92
|
+
From this package directory, run its tests without installing the Kimi application:
|
|
93
|
+
|
|
94
|
+
```console
|
|
95
|
+
uv run --isolated --group test python -m pytest -q
|
|
96
|
+
```
|
|
@@ -22,23 +22,58 @@ group:
|
|
|
22
22
|
|
|
23
23
|
```toml
|
|
24
24
|
[project]
|
|
25
|
-
dependencies = ["kimi-agent-module-api>=
|
|
25
|
+
dependencies = ["kimi-agent-module-api>=2,<3"]
|
|
26
26
|
|
|
27
27
|
[project.entry-points."kimi_agent.modules"]
|
|
28
28
|
my_module = "my_module_package:SPEC"
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
The source must pin the API contract it implements when constructing the
|
|
32
|
+
specification:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from kimi_agent_module_api import ModuleSpec
|
|
36
|
+
|
|
37
|
+
SPEC = ModuleSpec(
|
|
38
|
+
name="my_module",
|
|
39
|
+
version="0.1.0",
|
|
40
|
+
create=create,
|
|
41
|
+
api_version=2,
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`api_version` is a required keyword. Keep it as a literal rather than deriving
|
|
46
|
+
it from the installed SDK's `MODULE_API_VERSION`; unchanged module source must
|
|
47
|
+
not silently claim compatibility merely because it was rebuilt with a newer
|
|
48
|
+
SDK.
|
|
49
|
+
|
|
31
50
|
The [module guide](https://github.com/webhead2oo9/kimi-agent/blob/main/docs/modules.md)
|
|
32
51
|
documents installation, declarations, lifecycle, and every runtime port. The
|
|
33
52
|
[reference module](https://github.com/webhead2oo9/kimi-agent/tree/main/bot/modules/example)
|
|
34
53
|
is a complete, commented example that exercises most ports; start there.
|
|
35
54
|
|
|
36
55
|
Guild-scoped live command replacement was added in 1.1. Modules using
|
|
37
|
-
`InteractionRouter.replace_guild_commands()` should
|
|
38
|
-
`kimi-agent-module-api>=1.1,<2` and require the host capability
|
|
56
|
+
`InteractionRouter.replace_guild_commands()` should require the host capability
|
|
39
57
|
`discord.guild_commands.v1`.
|
|
40
58
|
|
|
41
59
|
Version 1.2 adds typed modal forms and a narrow Components V2 layout model. Once a response uses
|
|
42
60
|
that layout model, Discord requires every later edit of the same message to remain a layout.
|
|
43
|
-
Modules using them should
|
|
61
|
+
Modules using them should require
|
|
44
62
|
`discord.modals.v1` and/or `discord.components_v2.v1`.
|
|
63
|
+
|
|
64
|
+
Version 1.3 adds cached author classification to message-deletion events:
|
|
65
|
+
`MessageDeleteEvent.author_is_bot` and `MessageBulkDeleteEvent.bot_message_ids`.
|
|
66
|
+
The values remain unknown for messages that were absent from Discord's cache.
|
|
67
|
+
|
|
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.
|
|
72
|
+
|
|
73
|
+
## Testing the SDK
|
|
74
|
+
|
|
75
|
+
From this package directory, run its tests without installing the Kimi application:
|
|
76
|
+
|
|
77
|
+
```console
|
|
78
|
+
uv run --isolated --group test python -m pytest -q
|
|
79
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "kimi-agent-module-api"
|
|
3
|
-
version = "
|
|
3
|
+
version = "2.0.0"
|
|
4
4
|
description = "Stable contracts for community-built assistant modules"
|
|
5
5
|
requires-python = ">=3.14"
|
|
6
6
|
authors = [{ name = "Webhead" }]
|
|
@@ -18,6 +18,9 @@ Repository = "https://github.com/webhead2oo9/kimi-agent"
|
|
|
18
18
|
# kimi_agent_module_api.testing.MemoryStorage runs real SQL in unit tests.
|
|
19
19
|
testing = ["aiosqlite"]
|
|
20
20
|
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
test = ["aiosqlite", "pytest", "pytest-asyncio"]
|
|
23
|
+
|
|
21
24
|
[build-system]
|
|
22
25
|
requires = ["setuptools>=84"]
|
|
23
26
|
build-backend = "setuptools.build_meta"
|
|
@@ -27,3 +30,7 @@ where = ["src"]
|
|
|
27
30
|
|
|
28
31
|
[tool.setuptools.package-data]
|
|
29
32
|
kimi_agent_module_api = ["py.typed"]
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
testpaths = ["tests"]
|
|
36
|
+
asyncio_default_fixture_loop_scope = "function"
|
{kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/src/kimi_agent_module_api/__init__.py
RENAMED
|
@@ -3,23 +3,19 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from collections.abc import Callable, Mapping, Sequence
|
|
6
|
-
from dataclasses import dataclass, field
|
|
6
|
+
from dataclasses import KW_ONLY, dataclass, field
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any, Protocol, TypeVar
|
|
9
9
|
|
|
10
10
|
from pydantic_settings import BaseSettings
|
|
11
11
|
|
|
12
|
+
from kimi_agent_module_api import contracts as _contracts
|
|
12
13
|
from kimi_agent_module_api.contracts import (
|
|
13
14
|
ConfigSnapshot,
|
|
14
15
|
RoleSnapshot,
|
|
15
16
|
render_guild_settings,
|
|
16
|
-
DiscordActions,
|
|
17
|
-
EventBus,
|
|
18
|
-
GuildSettings,
|
|
19
17
|
GuildSettingsSchema,
|
|
20
|
-
HealthReporter,
|
|
21
18
|
InviteSnapshot,
|
|
22
|
-
InteractionRouter,
|
|
23
19
|
LayoutGallery,
|
|
24
20
|
LayoutItem,
|
|
25
21
|
LayoutSection,
|
|
@@ -27,9 +23,7 @@ from kimi_agent_module_api.contracts import (
|
|
|
27
23
|
LayoutSeparatorSpacing,
|
|
28
24
|
LayoutText,
|
|
29
25
|
ModalSpec,
|
|
30
|
-
ModuleHttp,
|
|
31
26
|
ModulePermissions,
|
|
32
|
-
ModuleStorage,
|
|
33
27
|
OutgoingLayout,
|
|
34
28
|
ScopedModuleMigration,
|
|
35
29
|
ProposalActor,
|
|
@@ -37,11 +31,8 @@ from kimi_agent_module_api.contracts import (
|
|
|
37
31
|
ProposalRef,
|
|
38
32
|
ProposalService,
|
|
39
33
|
ProposalState,
|
|
40
|
-
Scheduler,
|
|
41
34
|
ServiceDeclaration,
|
|
42
|
-
ServiceRegistry,
|
|
43
35
|
ServiceRequirement,
|
|
44
|
-
TrustLookup,
|
|
45
36
|
TextInputSpec,
|
|
46
37
|
TextInputStyle,
|
|
47
38
|
)
|
|
@@ -53,7 +44,7 @@ from kimi_agent_module_api.tools import (
|
|
|
53
44
|
)
|
|
54
45
|
from kimi_agent_module_api.trust import TrustTier
|
|
55
46
|
|
|
56
|
-
MODULE_API_VERSION =
|
|
47
|
+
MODULE_API_VERSION = 2
|
|
57
48
|
MODULE_ENTRYPOINT_GROUP = "kimi_agent.modules"
|
|
58
49
|
# Capabilities every compatible host advertises regardless of configuration.
|
|
59
50
|
BASELINE_CAPABILITIES: frozenset[str] = frozenset({"discord.history.v1", "proposals.v2"})
|
|
@@ -82,10 +73,13 @@ class AppModule(Protocol):
|
|
|
82
73
|
|
|
83
74
|
@dataclass(frozen=True)
|
|
84
75
|
class ModuleSpec:
|
|
76
|
+
"""A module declaration with an explicit, source-pinned host API version."""
|
|
77
|
+
|
|
85
78
|
name: str
|
|
86
79
|
version: str
|
|
87
80
|
create: Callable[[ModuleLoadContext], AppModule]
|
|
88
|
-
|
|
81
|
+
_: KW_ONLY
|
|
82
|
+
api_version: int
|
|
89
83
|
dependencies: tuple[str, ...] = ()
|
|
90
84
|
settings: ModuleSettingsDefinition | None = None
|
|
91
85
|
requires_capabilities: tuple[str, ...] = ()
|
|
@@ -94,7 +88,6 @@ class ModuleSpec:
|
|
|
94
88
|
guild_settings: GuildSettingsSchema | None = None
|
|
95
89
|
provides: tuple[ServiceDeclaration, ...] = ()
|
|
96
90
|
consumes: tuple[ServiceRequirement, ...] = ()
|
|
97
|
-
table_aliases: Mapping[str, str] = field(default_factory=dict)
|
|
98
91
|
|
|
99
92
|
|
|
100
93
|
@dataclass(frozen=True)
|
|
@@ -138,16 +131,16 @@ class ModuleRuntimeContext:
|
|
|
138
131
|
is_guild_active: Callable[[int], bool]
|
|
139
132
|
current_config_dir: Callable[[], Path]
|
|
140
133
|
capabilities: ModuleCapabilities
|
|
141
|
-
events: EventBus
|
|
142
|
-
scheduler: Scheduler
|
|
143
|
-
storage: ModuleStorage
|
|
144
|
-
health: HealthReporter
|
|
145
|
-
discord: DiscordActions
|
|
146
|
-
interactions: InteractionRouter
|
|
147
|
-
http: ModuleHttp
|
|
148
|
-
services: ServiceRegistry
|
|
149
|
-
trust: TrustLookup
|
|
150
|
-
guild_settings: GuildSettings | None = None
|
|
134
|
+
events: _contracts.EventBus
|
|
135
|
+
scheduler: _contracts.Scheduler
|
|
136
|
+
storage: _contracts.ModuleStorage
|
|
137
|
+
health: _contracts.HealthReporter
|
|
138
|
+
discord: _contracts.DiscordActions
|
|
139
|
+
interactions: _contracts.InteractionRouter
|
|
140
|
+
http: _contracts.ModuleHttp
|
|
141
|
+
services: _contracts.ServiceRegistry
|
|
142
|
+
trust: _contracts.TrustLookup
|
|
143
|
+
guild_settings: _contracts.GuildSettings | None = None
|
|
151
144
|
proposals: ProposalService | None = None
|
|
152
145
|
raw_bot: Any = None
|
|
153
146
|
raw_storage: Any = None
|
{kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/src/kimi_agent_module_api/contracts.py
RENAMED
|
@@ -12,6 +12,7 @@ owner manifest and enforced through the ports below; they are not a sandbox.
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
14
|
import json
|
|
15
|
+
import keyword
|
|
15
16
|
import math
|
|
16
17
|
import re
|
|
17
18
|
from collections.abc import AsyncIterator, Awaitable, Callable, Mapping, Sequence
|
|
@@ -326,6 +327,13 @@ CORE_TOPIC_PREFIX = "discord"
|
|
|
326
327
|
_CORE_RESERVED_MODULE_NAMES = frozenset({CORE_TOPIC_PREFIX, "proposals"})
|
|
327
328
|
CUSTOM_ID_PREFIX = "m"
|
|
328
329
|
CUSTOM_ID_MAX_LENGTH = 100
|
|
330
|
+
# A modal ID also carries a fixed-width per-open suffix so two people opening the
|
|
331
|
+
# same form do not share one entry in the host's modal table. That suffix is not
|
|
332
|
+
# the module's to spend, so a modal's declared ID has a smaller budget than a
|
|
333
|
+
# button's. Fixed width on purpose: a variable one would shrink the budget as the
|
|
334
|
+
# host ran, letting a modal open successfully and then fail later.
|
|
335
|
+
MODAL_NONCE_CHARS = 8
|
|
336
|
+
MODAL_CUSTOM_ID_MAX_LENGTH = CUSTOM_ID_MAX_LENGTH - 1 - MODAL_NONCE_CHARS
|
|
329
337
|
|
|
330
338
|
|
|
331
339
|
def table_prefix(module_name: str) -> str:
|
|
@@ -987,6 +995,7 @@ class TrustLookup(Protocol):
|
|
|
987
995
|
|
|
988
996
|
|
|
989
997
|
type CommandOptionKind = Literal["string", "integer", "boolean", "user", "channel", "role"]
|
|
998
|
+
_OPTION_KINDS = frozenset({"string", "integer", "boolean", "user", "channel", "role"})
|
|
990
999
|
|
|
991
1000
|
|
|
992
1001
|
@dataclass(frozen=True, slots=True)
|
|
@@ -1147,6 +1156,208 @@ class ModalSpec:
|
|
|
1147
1156
|
parts: tuple[str, ...] = ()
|
|
1148
1157
|
|
|
1149
1158
|
|
|
1159
|
+
_OPTIONS_WITH_CHOICES = frozenset({"string", "integer"})
|
|
1160
|
+
# Keep command and group name validation in the SDK so the public fakes reject
|
|
1161
|
+
# the same payloads discord.py rejects while constructing the live tree.
|
|
1162
|
+
_COMMAND_NAME_RE = re.compile(
|
|
1163
|
+
r"^[-_\w\u0e31-\u0e3a\u0e47-\u0e4e\u0900-\u0903\u093a\u093b\u093c"
|
|
1164
|
+
r"\u093e\u093f\u0940-\u094f\u0955\u0956\u0957\u0962\u0963]{1,32}$"
|
|
1165
|
+
)
|
|
1166
|
+
_BUTTON_STYLES = frozenset({"primary", "secondary", "success", "danger"})
|
|
1167
|
+
_DISCORD_INTEGER_MAX = (1 << 53) - 1
|
|
1168
|
+
_DISCORD_INTEGER_MIN = -_DISCORD_INTEGER_MAX
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
def validate_command_spec(spec: CommandSpec) -> None:
|
|
1172
|
+
"""Validate the Discord limits a command payload must satisfy.
|
|
1173
|
+
|
|
1174
|
+
A whole ``tree.sync()`` is one bulk PUT, so a single malformed command
|
|
1175
|
+
rejects every command in that scope. Discord.py checks command and group
|
|
1176
|
+
*names* itself and reorders required options ahead of optional ones; these
|
|
1177
|
+
are the limits nothing else enforces before the payload reaches Discord.
|
|
1178
|
+
"""
|
|
1179
|
+
|
|
1180
|
+
for label, name in (("command", spec.name), ("command group", spec.group)):
|
|
1181
|
+
if name is None:
|
|
1182
|
+
continue
|
|
1183
|
+
if (
|
|
1184
|
+
not isinstance(name, str)
|
|
1185
|
+
or _COMMAND_NAME_RE.fullmatch(name) is None
|
|
1186
|
+
or name.lower() != name
|
|
1187
|
+
):
|
|
1188
|
+
raise ModuleContractError(f"invalid {label} name {name!r}")
|
|
1189
|
+
if not isinstance(spec.description, str) or not 1 <= len(spec.description) <= 100:
|
|
1190
|
+
raise ModuleContractError("a command description must contain 1 to 100 characters")
|
|
1191
|
+
if spec.group is not None and (
|
|
1192
|
+
not isinstance(spec.group_description, str) or len(spec.group_description) > 100
|
|
1193
|
+
):
|
|
1194
|
+
raise ModuleContractError("a command group description cannot exceed 100 characters")
|
|
1195
|
+
if len(spec.options) > 25:
|
|
1196
|
+
raise ModuleContractError("a command cannot declare more than 25 options")
|
|
1197
|
+
|
|
1198
|
+
names: set[str] = set()
|
|
1199
|
+
for option in spec.options:
|
|
1200
|
+
# Discord applies the slash-command name grammar to option names, while
|
|
1201
|
+
# the host also builds each option into a Python parameter. Enforce the
|
|
1202
|
+
# intersection here: this keeps valid leading underscores and lowercase
|
|
1203
|
+
# Unicode identifiers while rejecting Discord-only names such as ``a-b``.
|
|
1204
|
+
if (
|
|
1205
|
+
not isinstance(option.name, str)
|
|
1206
|
+
or _COMMAND_NAME_RE.fullmatch(option.name) is None
|
|
1207
|
+
or option.name.lower() != option.name
|
|
1208
|
+
or not option.name.isidentifier()
|
|
1209
|
+
or keyword.iskeyword(option.name)
|
|
1210
|
+
):
|
|
1211
|
+
raise ModuleContractError(f"invalid command option name {option.name!r}")
|
|
1212
|
+
if option.name in names:
|
|
1213
|
+
raise ModuleContractError("command option names must be unique")
|
|
1214
|
+
names.add(option.name)
|
|
1215
|
+
if option.kind not in _OPTION_KINDS:
|
|
1216
|
+
raise ModuleContractError(f"unsupported command option kind {option.kind!r}")
|
|
1217
|
+
if not isinstance(option.description, str) or not 1 <= len(option.description) <= 100:
|
|
1218
|
+
raise ModuleContractError(
|
|
1219
|
+
f"the description for option {option.name!r} must contain 1 to 100 characters"
|
|
1220
|
+
)
|
|
1221
|
+
if option.choices and option.autocomplete:
|
|
1222
|
+
# Discord rejects a payload carrying both, and discord.py emits both.
|
|
1223
|
+
raise ModuleContractError(
|
|
1224
|
+
f"option {option.name!r} cannot use choices and autocomplete together"
|
|
1225
|
+
)
|
|
1226
|
+
if (option.choices or option.autocomplete) and option.kind not in _OPTIONS_WITH_CHOICES:
|
|
1227
|
+
raise ModuleContractError(
|
|
1228
|
+
f"option {option.name!r} cannot use choices or autocomplete on a "
|
|
1229
|
+
f"{option.kind} option"
|
|
1230
|
+
)
|
|
1231
|
+
if len(option.choices) > 25:
|
|
1232
|
+
raise ModuleContractError(f"option {option.name!r} cannot declare more than 25 choices")
|
|
1233
|
+
values: set[str | int] = set()
|
|
1234
|
+
for name, value in option.choices:
|
|
1235
|
+
if not isinstance(name, str) or not 1 <= len(name) <= 100:
|
|
1236
|
+
raise ModuleContractError(f"invalid choice name {name!r} on option {option.name!r}")
|
|
1237
|
+
if isinstance(value, bool) or not isinstance(value, str | int):
|
|
1238
|
+
raise ModuleContractError(
|
|
1239
|
+
f"invalid choice value {value!r} on option {option.name!r}"
|
|
1240
|
+
)
|
|
1241
|
+
# A choice whose value type disagrees with its option is a 400 on the
|
|
1242
|
+
# whole payload, and nothing downstream cross-checks the two.
|
|
1243
|
+
if not isinstance(value, str if option.kind == "string" else int):
|
|
1244
|
+
raise ModuleContractError(
|
|
1245
|
+
f"choice values on {option.kind} option {option.name!r} must be "
|
|
1246
|
+
f"{'strings' if option.kind == 'string' else 'integers'}"
|
|
1247
|
+
)
|
|
1248
|
+
if (
|
|
1249
|
+
option.kind == "integer"
|
|
1250
|
+
and isinstance(value, int)
|
|
1251
|
+
and not _DISCORD_INTEGER_MIN <= value <= _DISCORD_INTEGER_MAX
|
|
1252
|
+
):
|
|
1253
|
+
raise ModuleContractError(
|
|
1254
|
+
f"an integer choice value on option {option.name!r} must be between "
|
|
1255
|
+
f"{_DISCORD_INTEGER_MIN} and {_DISCORD_INTEGER_MAX}"
|
|
1256
|
+
)
|
|
1257
|
+
if isinstance(value, str) and not 1 <= len(value) <= 100:
|
|
1258
|
+
raise ModuleContractError(
|
|
1259
|
+
f"a choice value on option {option.name!r} must contain 1 to 100 characters"
|
|
1260
|
+
)
|
|
1261
|
+
if value in values:
|
|
1262
|
+
raise ModuleContractError(f"choice values on option {option.name!r} must be unique")
|
|
1263
|
+
values.add(value)
|
|
1264
|
+
for bound in (option.min_value, option.max_value):
|
|
1265
|
+
if bound is None:
|
|
1266
|
+
continue
|
|
1267
|
+
if isinstance(bound, bool) or not isinstance(bound, int):
|
|
1268
|
+
raise ModuleContractError(f"option {option.name!r} bounds must be integers")
|
|
1269
|
+
if option.kind != "integer":
|
|
1270
|
+
# Only integer options carry the bounds through to Discord, so a
|
|
1271
|
+
# bound anywhere else would be silently dropped.
|
|
1272
|
+
raise ModuleContractError(
|
|
1273
|
+
f"option {option.name!r} cannot set min_value or max_value on a "
|
|
1274
|
+
f"{option.kind} option"
|
|
1275
|
+
)
|
|
1276
|
+
if not _DISCORD_INTEGER_MIN <= bound <= _DISCORD_INTEGER_MAX:
|
|
1277
|
+
raise ModuleContractError(
|
|
1278
|
+
f"option {option.name!r} bounds must be between "
|
|
1279
|
+
f"{_DISCORD_INTEGER_MIN} and {_DISCORD_INTEGER_MAX}"
|
|
1280
|
+
)
|
|
1281
|
+
if (
|
|
1282
|
+
option.min_value is not None
|
|
1283
|
+
and option.max_value is not None
|
|
1284
|
+
and option.min_value > option.max_value
|
|
1285
|
+
):
|
|
1286
|
+
raise ModuleContractError(f"option {option.name!r} min_value cannot exceed max_value")
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
def validate_button_spec(button: ButtonSpec) -> None:
|
|
1290
|
+
"""Validate Discord's hard button limits."""
|
|
1291
|
+
|
|
1292
|
+
if not isinstance(button.key, str) or not _TOPIC_SEGMENT_RE.fullmatch(button.key):
|
|
1293
|
+
raise ModuleContractError(f"invalid button key {button.key!r}")
|
|
1294
|
+
if any(not isinstance(part, str) or ":" in part for part in button.parts):
|
|
1295
|
+
raise ModuleContractError("button custom_id parts must be strings without ':'")
|
|
1296
|
+
if not isinstance(button.label, str) or len(button.label) > 80:
|
|
1297
|
+
raise ModuleContractError("a button label cannot exceed 80 characters")
|
|
1298
|
+
if button.style not in _BUTTON_STYLES:
|
|
1299
|
+
raise ModuleContractError(f"invalid button style {button.style!r}")
|
|
1300
|
+
if button.emoji is not None and (not isinstance(button.emoji, str) or not button.emoji):
|
|
1301
|
+
raise ModuleContractError("a button emoji must be a non-empty string")
|
|
1302
|
+
if not button.label and button.emoji is None:
|
|
1303
|
+
raise ModuleContractError("a button must provide a label or emoji")
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
def validate_component_spec(component: Any) -> None:
|
|
1307
|
+
"""Validate one interactive component, whichever kind it is."""
|
|
1308
|
+
|
|
1309
|
+
if isinstance(component, ButtonSpec):
|
|
1310
|
+
validate_button_spec(component)
|
|
1311
|
+
elif isinstance(component, SelectSpec):
|
|
1312
|
+
validate_select_spec(component)
|
|
1313
|
+
else:
|
|
1314
|
+
raise ModuleContractError(f"unsupported component {component!r}")
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
def validate_select_spec(select: SelectSpec) -> None:
|
|
1318
|
+
"""Validate Discord's hard select-menu limits."""
|
|
1319
|
+
|
|
1320
|
+
if not isinstance(select.key, str) or not _TOPIC_SEGMENT_RE.fullmatch(select.key):
|
|
1321
|
+
raise ModuleContractError(f"invalid select key {select.key!r}")
|
|
1322
|
+
if any(not isinstance(part, str) or ":" in part for part in select.parts):
|
|
1323
|
+
raise ModuleContractError("select custom_id parts must be strings without ':'")
|
|
1324
|
+
if select.placeholder is not None and (
|
|
1325
|
+
not isinstance(select.placeholder, str) or len(select.placeholder) > 150
|
|
1326
|
+
):
|
|
1327
|
+
raise ModuleContractError("a select placeholder cannot exceed 150 characters")
|
|
1328
|
+
if not 1 <= len(select.options) <= 25:
|
|
1329
|
+
raise ModuleContractError("a select must contain between one and 25 options")
|
|
1330
|
+
|
|
1331
|
+
values: set[str] = set()
|
|
1332
|
+
for label, value, description in select.options:
|
|
1333
|
+
if not isinstance(label, str) or not 1 <= len(label) <= 100:
|
|
1334
|
+
raise ModuleContractError("a select option label must contain 1 to 100 characters")
|
|
1335
|
+
if not isinstance(value, str) or not 1 <= len(value) <= 100:
|
|
1336
|
+
raise ModuleContractError("a select option value must contain 1 to 100 characters")
|
|
1337
|
+
if value in values:
|
|
1338
|
+
raise ModuleContractError("select option values must be unique")
|
|
1339
|
+
values.add(value)
|
|
1340
|
+
if description is not None and (
|
|
1341
|
+
not isinstance(description, str) or not 1 <= len(description) <= 100
|
|
1342
|
+
):
|
|
1343
|
+
raise ModuleContractError(
|
|
1344
|
+
"a select option description must contain 1 to 100 characters"
|
|
1345
|
+
)
|
|
1346
|
+
|
|
1347
|
+
for name, bound, low in (
|
|
1348
|
+
("min_values", select.min_values, 0),
|
|
1349
|
+
("max_values", select.max_values, 1),
|
|
1350
|
+
):
|
|
1351
|
+
if isinstance(bound, bool) or not isinstance(bound, int) or not low <= bound <= 25:
|
|
1352
|
+
raise ModuleContractError(f"select {name} must be between {low} and 25")
|
|
1353
|
+
if select.min_values > select.max_values:
|
|
1354
|
+
raise ModuleContractError("select min_values cannot exceed max_values")
|
|
1355
|
+
# max_values may exceed the option count (Discord clamps it), but a
|
|
1356
|
+
# minimum above the count can never be satisfied, so the select is dead.
|
|
1357
|
+
if select.min_values > len(select.options):
|
|
1358
|
+
raise ModuleContractError("select min_values cannot exceed the number of options")
|
|
1359
|
+
|
|
1360
|
+
|
|
1150
1361
|
def validate_modal_spec(modal: ModalSpec) -> None:
|
|
1151
1362
|
"""Validate Discord's hard modal and text-input limits."""
|
|
1152
1363
|
if not isinstance(modal.key, str) or not _TOPIC_SEGMENT_RE.fullmatch(modal.key):
|
|
@@ -1255,7 +1466,6 @@ class GuildSettingsSnapshot:
|
|
|
1255
1466
|
valid: bool
|
|
1256
1467
|
errors: tuple[str, ...]
|
|
1257
1468
|
revision: str
|
|
1258
|
-
legacy: bool = False
|
|
1259
1469
|
|
|
1260
1470
|
|
|
1261
1471
|
class GuildSettings(Protocol):
|
{kimi_agent_module_api-1.2.0 → kimi_agent_module_api-2.0.0}/src/kimi_agent_module_api/events.py
RENAMED
|
@@ -69,11 +69,13 @@ class MessageDeleteEvent:
|
|
|
69
69
|
author_id: int | None
|
|
70
70
|
cached_content: str | None
|
|
71
71
|
cached_attachments: tuple[AttachmentSnapshot, ...]
|
|
72
|
+
author_is_bot: bool | None = None
|
|
72
73
|
|
|
73
74
|
|
|
74
75
|
@dataclass(frozen=True, slots=True)
|
|
75
76
|
class MessageBulkDeleteEvent:
|
|
76
77
|
refs: tuple[MessageRef, ...]
|
|
78
|
+
bot_message_ids: tuple[int, ...] = ()
|
|
77
79
|
|
|
78
80
|
|
|
79
81
|
@dataclass(frozen=True, slots=True)
|