kimi-agent-module-api 2.3.0__tar.gz → 2.4.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.3.0/src/kimi_agent_module_api.egg-info → kimi_agent_module_api-2.4.0}/PKG-INFO +14 -1
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/README.md +13 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/pyproject.toml +1 -1
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api/__init__.py +17 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api/contracts.py +7 -0
- kimi_agent_module_api-2.4.0/src/kimi_agent_module_api/scheduled_results.py +105 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api/testing.py +98 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0/src/kimi_agent_module_api.egg-info}/PKG-INFO +14 -1
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api.egg-info/SOURCES.txt +2 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/tests/test_contracts.py +2 -2
- kimi_agent_module_api-2.4.0/tests/test_scheduled_results.py +99 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/LICENSE +0 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/setup.cfg +0 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api/events.py +0 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api/files.py +0 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api/images.py +0 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api/py.typed +0 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api/settings.py +0 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api/tools.py +0 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api/trust.py +0 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api.egg-info/dependency_links.txt +0 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api.egg-info/requires.txt +0 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api.egg-info/top_level.txt +0 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/tests/test_files.py +0 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/tests/test_public_api.py +0 -0
- {kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.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.4.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.scheduled_results`: durable subscriptions to published
|
|
34
|
+
task output, typed results, and bounded attachment readers (SDK 2.4).
|
|
33
35
|
- `kimi_agent_module_api.files`: `ToolFiles`, `ToolAttachment`, `ToolFile`, and
|
|
34
36
|
`FileAccessError` for invocation-scoped attachment and workspace reads.
|
|
35
37
|
- `kimi_agent_module_api.testing`: a fake for every port, `load_context()` for
|
|
@@ -116,3 +118,14 @@ From this package directory, run its tests without installing the Kimi applicati
|
|
|
116
118
|
```console
|
|
117
119
|
uv run --isolated --group test python -m pytest -q
|
|
118
120
|
```
|
|
121
|
+
|
|
122
|
+
SDK 2.4 adds `ScheduledResults`: named, guild-scoped subscriptions to task output
|
|
123
|
+
following confirmed Discord publication. Declare
|
|
124
|
+
`ModulePermissions(scheduled_results=("my_subscription",))`, require the host
|
|
125
|
+
capability `scheduled_results.v1`, and register through `ctx.scheduled_results`
|
|
126
|
+
during startup. Handlers receive a typed `ScheduledResult` and an invocation-scoped
|
|
127
|
+
`ScheduledResultFiles` reader. Returning acknowledges the result; exceptions retry
|
|
128
|
+
with the same notification ID. Use that ID for idempotent processing.
|
|
129
|
+
`FakeScheduledResults` supports standalone tests. See the
|
|
130
|
+
[published-result guide](https://github.com/Kimi-Discord-Agent/kimi-agent/blob/main/docs/module-scheduled-results.md)
|
|
131
|
+
for access checks, limits, retention, and deletion responsibilities.
|
|
@@ -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.scheduled_results`: durable subscriptions to published
|
|
17
|
+
task output, typed results, and bounded attachment readers (SDK 2.4).
|
|
16
18
|
- `kimi_agent_module_api.files`: `ToolFiles`, `ToolAttachment`, `ToolFile`, and
|
|
17
19
|
`FileAccessError` for invocation-scoped attachment and workspace reads.
|
|
18
20
|
- `kimi_agent_module_api.testing`: a fake for every port, `load_context()` for
|
|
@@ -99,3 +101,14 @@ From this package directory, run its tests without installing the Kimi applicati
|
|
|
99
101
|
```console
|
|
100
102
|
uv run --isolated --group test python -m pytest -q
|
|
101
103
|
```
|
|
104
|
+
|
|
105
|
+
SDK 2.4 adds `ScheduledResults`: named, guild-scoped subscriptions to task output
|
|
106
|
+
following confirmed Discord publication. Declare
|
|
107
|
+
`ModulePermissions(scheduled_results=("my_subscription",))`, require the host
|
|
108
|
+
capability `scheduled_results.v1`, and register through `ctx.scheduled_results`
|
|
109
|
+
during startup. Handlers receive a typed `ScheduledResult` and an invocation-scoped
|
|
110
|
+
`ScheduledResultFiles` reader. Returning acknowledges the result; exceptions retry
|
|
111
|
+
with the same notification ID. Use that ID for idempotent processing.
|
|
112
|
+
`FakeScheduledResults` supports standalone tests. See the
|
|
113
|
+
[published-result guide](https://github.com/Kimi-Discord-Agent/kimi-agent/blob/main/docs/module-scheduled-results.md)
|
|
114
|
+
for access checks, limits, retention, and deletion responsibilities.
|
{kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api/__init__.py
RENAMED
|
@@ -46,6 +46,15 @@ from kimi_agent_module_api.tools import (
|
|
|
46
46
|
TriggeringDiscordMessageSnapshot,
|
|
47
47
|
)
|
|
48
48
|
from kimi_agent_module_api.trust import TrustTier
|
|
49
|
+
from kimi_agent_module_api.scheduled_results import (
|
|
50
|
+
ScheduledResult,
|
|
51
|
+
ScheduledResultAccessError,
|
|
52
|
+
ScheduledResultAttachment,
|
|
53
|
+
ScheduledResultFiles,
|
|
54
|
+
ScheduledResultHandler,
|
|
55
|
+
ScheduledResultMessage,
|
|
56
|
+
ScheduledResults,
|
|
57
|
+
)
|
|
49
58
|
|
|
50
59
|
MODULE_API_VERSION = 2
|
|
51
60
|
MODULE_ENTRYPOINT_GROUP = "kimi_agent.modules"
|
|
@@ -147,6 +156,7 @@ class ModuleRuntimeContext:
|
|
|
147
156
|
proposals: ProposalService | None = None
|
|
148
157
|
raw_bot: Any = None
|
|
149
158
|
raw_storage: Any = None
|
|
159
|
+
scheduled_results: ScheduledResults | None = None
|
|
150
160
|
|
|
151
161
|
|
|
152
162
|
__all__ = [
|
|
@@ -183,6 +193,13 @@ __all__ = [
|
|
|
183
193
|
"ProposalService",
|
|
184
194
|
"ProposalState",
|
|
185
195
|
"RoleSnapshot",
|
|
196
|
+
"ScheduledResult",
|
|
197
|
+
"ScheduledResultAccessError",
|
|
198
|
+
"ScheduledResultAttachment",
|
|
199
|
+
"ScheduledResultFiles",
|
|
200
|
+
"ScheduledResultHandler",
|
|
201
|
+
"ScheduledResultMessage",
|
|
202
|
+
"ScheduledResults",
|
|
186
203
|
"ScopedModuleMigration",
|
|
187
204
|
"ServiceDeclaration",
|
|
188
205
|
"ServiceRequirement",
|
{kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api/contracts.py
RENAMED
|
@@ -145,6 +145,8 @@ class ModulePermissions:
|
|
|
145
145
|
raw_storage: bool = False
|
|
146
146
|
# Read admitted attachments and caller-owned workspace files in tool handlers.
|
|
147
147
|
tool_files: bool = False
|
|
148
|
+
# Names of durable, explicitly guild-scoped published-task subscriptions.
|
|
149
|
+
scheduled_results: tuple[str, ...] = ()
|
|
148
150
|
|
|
149
151
|
|
|
150
152
|
@dataclass(frozen=True, slots=True)
|
|
@@ -439,6 +441,11 @@ def validate_permissions(module_name: str, permissions: ModulePermissions) -> No
|
|
|
439
441
|
)
|
|
440
442
|
for rule in permissions.http_hosts:
|
|
441
443
|
validate_host_rule(rule)
|
|
444
|
+
names = permissions.scheduled_results
|
|
445
|
+
if len(set(names)) != len(names) or any(
|
|
446
|
+
not re.fullmatch(r"[a-z0-9_]{1,64}", name) for name in names
|
|
447
|
+
):
|
|
448
|
+
raise ModuleContractError("scheduled-result subscription names must be unique and valid")
|
|
442
449
|
|
|
443
450
|
|
|
444
451
|
def validate_services(
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Durable subscriptions to confirmed Discord publications, separate from module jobs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Literal, Protocol
|
|
8
|
+
|
|
9
|
+
from kimi_agent_module_api.contracts import MessageRef, ModuleContractError
|
|
10
|
+
|
|
11
|
+
MAX_RESULT_FILE_BYTES = 8 * 1024 * 1024
|
|
12
|
+
MAX_RESULT_TOTAL_FILE_BYTES = 16 * 1024 * 1024
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ScheduledResultAccessError(ModuleContractError):
|
|
16
|
+
"""The subscription or its retained output is unavailable to this caller."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class ScheduledResultAttachment:
|
|
21
|
+
id: str
|
|
22
|
+
filename: str
|
|
23
|
+
size_bytes: int
|
|
24
|
+
description: str | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class ScheduledResultMessage:
|
|
29
|
+
message: MessageRef
|
|
30
|
+
content: str
|
|
31
|
+
attachments: tuple[ScheduledResultAttachment, ...] = ()
|
|
32
|
+
# The published Discord embed as JSON, capped at 32 KiB. No network fetches.
|
|
33
|
+
embed_json: str | None = None
|
|
34
|
+
embed_unavailable_reason: str | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class ScheduledResult:
|
|
39
|
+
notification_id: str
|
|
40
|
+
subscription: str
|
|
41
|
+
task_id: str
|
|
42
|
+
run_id: str
|
|
43
|
+
revision: int
|
|
44
|
+
guild_id: int
|
|
45
|
+
owner_id: int
|
|
46
|
+
published_at: float
|
|
47
|
+
expires_at: float
|
|
48
|
+
messages: tuple[ScheduledResultMessage, ...]
|
|
49
|
+
outcome: Literal["completed"] = "completed"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ScheduledResultFiles(Protocol):
|
|
53
|
+
"""Read only this notification's attachments during its handler invocation.
|
|
54
|
+
|
|
55
|
+
The host rechecks access on every read. Reads are capped at 8 MiB each and
|
|
56
|
+
16 MiB total per invocation, including repeated reads. Handles expire on
|
|
57
|
+
return, cancellation, deletion, or retention expiry; copy bytes deliberately.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
async def read(
|
|
61
|
+
self, attachment_id: str, *, max_bytes: int = MAX_RESULT_FILE_BYTES
|
|
62
|
+
) -> bytes: ...
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
type ScheduledResultHandler = Callable[[ScheduledResult, ScheduledResultFiles], Awaitable[None]]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ScheduledResults(Protocol):
|
|
69
|
+
"""A module-bound port. Declare each name in ``permissions.scheduled_results``.
|
|
70
|
+
|
|
71
|
+
Register on every start. A name is scoped to one guild per registration;
|
|
72
|
+
the same name may be registered in several guilds. Existing subscriptions
|
|
73
|
+
survive downtime; new registrations never replay historical publications.
|
|
74
|
+
Returning acknowledges the notification. Raising retries with the same ID.
|
|
75
|
+
Use that ID to make your own writes/remote requests idempotent.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
async def subscribe(
|
|
79
|
+
self, name: str, *, guild_id: int, handler: ScheduledResultHandler
|
|
80
|
+
) -> None: ...
|
|
81
|
+
|
|
82
|
+
async def unsubscribe(self, name: str, *, guild_id: int) -> None:
|
|
83
|
+
"""Forget this subscription and its pending/acknowledged notifications."""
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def validate_result_subscription(name: str, guild_id: int) -> None:
|
|
88
|
+
# Keep validation here usable by the host and standalone test fakes.
|
|
89
|
+
if (
|
|
90
|
+
not isinstance(name, str)
|
|
91
|
+
or not 1 <= len(name) <= 64
|
|
92
|
+
or any(char not in "abcdefghijklmnopqrstuvwxyz0123456789_" for char in name)
|
|
93
|
+
):
|
|
94
|
+
raise ModuleContractError("scheduled-result names use 1-64 lowercase letters, digits or _")
|
|
95
|
+
if isinstance(guild_id, bool) or not isinstance(guild_id, int) or not 0 < guild_id < 2**64:
|
|
96
|
+
raise ModuleContractError("scheduled-result subscriptions require a positive guild ID")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def validate_result_read_limit(max_bytes: int) -> None:
|
|
100
|
+
if (
|
|
101
|
+
isinstance(max_bytes, bool)
|
|
102
|
+
or not isinstance(max_bytes, int)
|
|
103
|
+
or not (0 < max_bytes <= MAX_RESULT_FILE_BYTES)
|
|
104
|
+
):
|
|
105
|
+
raise ScheduledResultAccessError("attachment read limit must be between 1 byte and 8 MiB")
|
{kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api/testing.py
RENAMED
|
@@ -24,6 +24,15 @@ from pydantic_settings import BaseSettings
|
|
|
24
24
|
|
|
25
25
|
from kimi_agent_module_api.trust import TrustTier
|
|
26
26
|
from kimi_agent_module_api.files import FileAccessError, ToolAttachment, ToolFile
|
|
27
|
+
from kimi_agent_module_api.scheduled_results import (
|
|
28
|
+
MAX_RESULT_FILE_BYTES,
|
|
29
|
+
MAX_RESULT_TOTAL_FILE_BYTES,
|
|
30
|
+
ScheduledResult,
|
|
31
|
+
ScheduledResultAccessError,
|
|
32
|
+
ScheduledResultHandler,
|
|
33
|
+
validate_result_read_limit,
|
|
34
|
+
validate_result_subscription,
|
|
35
|
+
)
|
|
27
36
|
|
|
28
37
|
from kimi_agent_module_api.contracts import (
|
|
29
38
|
ALL_DISCORD_ACTIONS,
|
|
@@ -85,6 +94,93 @@ _MAX_FLOAT_LOG = math.log(sys.float_info.max)
|
|
|
85
94
|
_T = TypeVar("_T")
|
|
86
95
|
|
|
87
96
|
|
|
97
|
+
class FakeScheduledResultFiles:
|
|
98
|
+
"""An invocation-scoped reader; the same size/lifetime limits as the host."""
|
|
99
|
+
|
|
100
|
+
def __init__(
|
|
101
|
+
self, files: Mapping[str, bytes], *, available: Callable[[], bool] = lambda: True
|
|
102
|
+
) -> None:
|
|
103
|
+
self.files = files
|
|
104
|
+
self.available = available
|
|
105
|
+
self.open = True
|
|
106
|
+
self.remaining = MAX_RESULT_TOTAL_FILE_BYTES
|
|
107
|
+
|
|
108
|
+
async def read(self, attachment_id: str, *, max_bytes: int = MAX_RESULT_FILE_BYTES) -> bytes:
|
|
109
|
+
validate_result_read_limit(max_bytes)
|
|
110
|
+
data = self.files.get(attachment_id)
|
|
111
|
+
if (
|
|
112
|
+
not self.open
|
|
113
|
+
or not self.available()
|
|
114
|
+
or self.remaining <= 0
|
|
115
|
+
or data is None
|
|
116
|
+
or len(data) > min(max_bytes, self.remaining)
|
|
117
|
+
):
|
|
118
|
+
raise ScheduledResultAccessError("Result file unavailable or read limit exceeded")
|
|
119
|
+
self.remaining -= len(data)
|
|
120
|
+
return data
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class FakeScheduledResults:
|
|
124
|
+
"""Drive retries explicitly with ``deliver``; successful IDs are acknowledged.
|
|
125
|
+
|
|
126
|
+
A failed invocation propagates its exception and can be delivered again.
|
|
127
|
+
Supply the original result object to model a restart or duplicate attempt.
|
|
128
|
+
This fake has no background runner, Discord access, or persistence.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def __init__(
|
|
132
|
+
self,
|
|
133
|
+
declared: tuple[str, ...] = (),
|
|
134
|
+
*,
|
|
135
|
+
is_guild_active: Callable[[int], bool] = lambda _guild_id: True,
|
|
136
|
+
) -> None:
|
|
137
|
+
self.declared, self.is_guild_active = declared, is_guild_active
|
|
138
|
+
self.handlers: dict[tuple[str, int], ScheduledResultHandler] = {}
|
|
139
|
+
self.acknowledged: set[str] = set()
|
|
140
|
+
self.attempts: list[str] = []
|
|
141
|
+
|
|
142
|
+
def _validate(self, name: str, guild_id: int) -> None:
|
|
143
|
+
validate_result_subscription(name, guild_id)
|
|
144
|
+
if name not in self.declared:
|
|
145
|
+
raise ScheduledResultAccessError("Undeclared scheduled-result subscription")
|
|
146
|
+
|
|
147
|
+
async def subscribe(self, name: str, *, guild_id: int, handler: ScheduledResultHandler) -> None:
|
|
148
|
+
self._validate(name, guild_id)
|
|
149
|
+
self.handlers[name, guild_id] = handler
|
|
150
|
+
|
|
151
|
+
async def unsubscribe(self, name: str, *, guild_id: int) -> None:
|
|
152
|
+
self._validate(name, guild_id)
|
|
153
|
+
self.handlers.pop((name, guild_id), None)
|
|
154
|
+
|
|
155
|
+
async def deliver(
|
|
156
|
+
self,
|
|
157
|
+
result: ScheduledResult,
|
|
158
|
+
*,
|
|
159
|
+
files: Mapping[str, bytes] | None = None,
|
|
160
|
+
preview: bool = False,
|
|
161
|
+
) -> None:
|
|
162
|
+
if preview or result.notification_id in self.acknowledged:
|
|
163
|
+
return
|
|
164
|
+
handler = self.handlers.get((result.subscription, result.guild_id))
|
|
165
|
+
if handler is None or not self.is_guild_active(result.guild_id):
|
|
166
|
+
raise ScheduledResultAccessError("Subscriber unavailable in this guild")
|
|
167
|
+
visible = {a.id for m in result.messages for a in m.attachments}
|
|
168
|
+
reader = FakeScheduledResultFiles(
|
|
169
|
+
{k: v for k, v in (files or {}).items() if k in visible},
|
|
170
|
+
available=lambda: (
|
|
171
|
+
self.is_guild_active(result.guild_id)
|
|
172
|
+
and result.subscription in self.declared
|
|
173
|
+
and self.handlers.get((result.subscription, result.guild_id)) is handler
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
self.attempts.append(result.notification_id)
|
|
177
|
+
try:
|
|
178
|
+
await handler(result, reader)
|
|
179
|
+
self.acknowledged.add(result.notification_id)
|
|
180
|
+
finally:
|
|
181
|
+
reader.open = False
|
|
182
|
+
|
|
183
|
+
|
|
88
184
|
@dataclass(slots=True)
|
|
89
185
|
class _Closable:
|
|
90
186
|
_on_close: Callable[[], object]
|
|
@@ -1390,6 +1486,8 @@ __all__ = [
|
|
|
1390
1486
|
"FakeInteractions",
|
|
1391
1487
|
"FakeProposals",
|
|
1392
1488
|
"FakeResponse",
|
|
1489
|
+
"FakeScheduledResultFiles",
|
|
1490
|
+
"FakeScheduledResults",
|
|
1393
1491
|
"FakeScheduler",
|
|
1394
1492
|
"FakeServiceRegistry",
|
|
1395
1493
|
"FakeToolFiles",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kimi-agent-module-api
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.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.scheduled_results`: durable subscriptions to published
|
|
34
|
+
task output, typed results, and bounded attachment readers (SDK 2.4).
|
|
33
35
|
- `kimi_agent_module_api.files`: `ToolFiles`, `ToolAttachment`, `ToolFile`, and
|
|
34
36
|
`FileAccessError` for invocation-scoped attachment and workspace reads.
|
|
35
37
|
- `kimi_agent_module_api.testing`: a fake for every port, `load_context()` for
|
|
@@ -116,3 +118,14 @@ From this package directory, run its tests without installing the Kimi applicati
|
|
|
116
118
|
```console
|
|
117
119
|
uv run --isolated --group test python -m pytest -q
|
|
118
120
|
```
|
|
121
|
+
|
|
122
|
+
SDK 2.4 adds `ScheduledResults`: named, guild-scoped subscriptions to task output
|
|
123
|
+
following confirmed Discord publication. Declare
|
|
124
|
+
`ModulePermissions(scheduled_results=("my_subscription",))`, require the host
|
|
125
|
+
capability `scheduled_results.v1`, and register through `ctx.scheduled_results`
|
|
126
|
+
during startup. Handlers receive a typed `ScheduledResult` and an invocation-scoped
|
|
127
|
+
`ScheduledResultFiles` reader. Returning acknowledges the result; exceptions retry
|
|
128
|
+
with the same notification ID. Use that ID for idempotent processing.
|
|
129
|
+
`FakeScheduledResults` supports standalone tests. See the
|
|
130
|
+
[published-result guide](https://github.com/Kimi-Discord-Agent/kimi-agent/blob/main/docs/module-scheduled-results.md)
|
|
131
|
+
for access checks, limits, retention, and deletion responsibilities.
|
|
@@ -7,6 +7,7 @@ src/kimi_agent_module_api/events.py
|
|
|
7
7
|
src/kimi_agent_module_api/files.py
|
|
8
8
|
src/kimi_agent_module_api/images.py
|
|
9
9
|
src/kimi_agent_module_api/py.typed
|
|
10
|
+
src/kimi_agent_module_api/scheduled_results.py
|
|
10
11
|
src/kimi_agent_module_api/settings.py
|
|
11
12
|
src/kimi_agent_module_api/testing.py
|
|
12
13
|
src/kimi_agent_module_api/tools.py
|
|
@@ -19,4 +20,5 @@ src/kimi_agent_module_api.egg-info/top_level.txt
|
|
|
19
20
|
tests/test_contracts.py
|
|
20
21
|
tests/test_files.py
|
|
21
22
|
tests/test_public_api.py
|
|
23
|
+
tests/test_scheduled_results.py
|
|
22
24
|
tests/test_testing.py
|
|
@@ -84,8 +84,8 @@ def test_spec_and_runtime_context_keep_stable_defaults() -> None:
|
|
|
84
84
|
assert {"events", "scheduler", "storage", "discord", "interactions", "services"} <= required
|
|
85
85
|
|
|
86
86
|
|
|
87
|
-
def
|
|
88
|
-
assert version("kimi-agent-module-api") == "2.
|
|
87
|
+
def test_distribution_version_includes_scheduled_result_contract() -> None:
|
|
88
|
+
assert version("kimi-agent-module-api") == "2.4.0"
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
def test_triggering_discord_message_snapshot_is_immutable() -> None:
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Exercise the public subscriber contract without importing a host."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import FrozenInstanceError, replace
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from kimi_agent_module_api import (
|
|
10
|
+
ModulePermissions,
|
|
11
|
+
ScheduledResult,
|
|
12
|
+
ScheduledResultAccessError,
|
|
13
|
+
ScheduledResultAttachment,
|
|
14
|
+
ScheduledResultMessage,
|
|
15
|
+
)
|
|
16
|
+
from kimi_agent_module_api.contracts import MessageRef, ModuleContractError, validate_permissions
|
|
17
|
+
from kimi_agent_module_api.testing import FakeScheduledResults
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def result():
|
|
21
|
+
return ScheduledResult(
|
|
22
|
+
notification_id="notification",
|
|
23
|
+
subscription="reports",
|
|
24
|
+
task_id="task",
|
|
25
|
+
run_id="run",
|
|
26
|
+
revision=3,
|
|
27
|
+
guild_id=100,
|
|
28
|
+
owner_id=10,
|
|
29
|
+
published_at=1000,
|
|
30
|
+
expires_at=2000,
|
|
31
|
+
messages=(
|
|
32
|
+
ScheduledResultMessage(
|
|
33
|
+
MessageRef(100, 300, 900),
|
|
34
|
+
"Published",
|
|
35
|
+
(ScheduledResultAttachment("file", "result.txt", 3),),
|
|
36
|
+
),
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_result_is_immutable_and_permissions_must_be_named():
|
|
42
|
+
with pytest.raises(FrozenInstanceError):
|
|
43
|
+
result().owner_id = 99
|
|
44
|
+
validate_permissions("reports", ModulePermissions(scheduled_results=("reports",)))
|
|
45
|
+
for names in (("Upper Case",), ("reports", "reports"), ("",)):
|
|
46
|
+
with pytest.raises(ModuleContractError):
|
|
47
|
+
validate_permissions("reports", ModulePermissions(scheduled_results=names))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.asyncio
|
|
51
|
+
async def test_fake_retry_acknowledgement_preview_and_file_lifetime():
|
|
52
|
+
fake = FakeScheduledResults(("reports",))
|
|
53
|
+
attempts, readers = [], []
|
|
54
|
+
|
|
55
|
+
async def handler(notification, files):
|
|
56
|
+
attempts.append(notification.notification_id)
|
|
57
|
+
readers.append(files)
|
|
58
|
+
assert await files.read("file") == b"abc"
|
|
59
|
+
with pytest.raises(ScheduledResultAccessError):
|
|
60
|
+
await files.read("not published")
|
|
61
|
+
with pytest.raises(ScheduledResultAccessError):
|
|
62
|
+
await files.read("file", max_bytes=2)
|
|
63
|
+
if len(attempts) == 1:
|
|
64
|
+
raise RuntimeError("retry me")
|
|
65
|
+
|
|
66
|
+
await fake.subscribe("reports", guild_id=100, handler=handler)
|
|
67
|
+
await fake.deliver(result(), preview=True)
|
|
68
|
+
assert attempts == []
|
|
69
|
+
files = {"file": b"abc", "not published": b"private"}
|
|
70
|
+
with pytest.raises(RuntimeError, match="retry me"):
|
|
71
|
+
await fake.deliver(result(), files=files)
|
|
72
|
+
assert not fake.acknowledged
|
|
73
|
+
await fake.deliver(result(), files=files)
|
|
74
|
+
await fake.deliver(result(), files=files)
|
|
75
|
+
assert attempts == ["notification", "notification"]
|
|
76
|
+
assert fake.acknowledged == {"notification"}
|
|
77
|
+
for reader in readers:
|
|
78
|
+
with pytest.raises(ScheduledResultAccessError):
|
|
79
|
+
await reader.read("file")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.mark.asyncio
|
|
83
|
+
async def test_fake_permission_and_guild_scope():
|
|
84
|
+
fake = FakeScheduledResults(("reports",), is_guild_active=lambda guild: guild == 100)
|
|
85
|
+
|
|
86
|
+
async def handler(_result, _files):
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
with pytest.raises(ScheduledResultAccessError):
|
|
90
|
+
await fake.subscribe("other", guild_id=100, handler=handler)
|
|
91
|
+
with pytest.raises(ModuleContractError):
|
|
92
|
+
await fake.subscribe("reports", guild_id=0, handler=handler)
|
|
93
|
+
await fake.subscribe("reports", guild_id=200, handler=handler)
|
|
94
|
+
with pytest.raises(ScheduledResultAccessError):
|
|
95
|
+
await fake.deliver(replace(result(), guild_id=200))
|
|
96
|
+
await fake.subscribe("reports", guild_id=100, handler=handler)
|
|
97
|
+
await fake.unsubscribe("reports", guild_id=100)
|
|
98
|
+
with pytest.raises(ScheduledResultAccessError):
|
|
99
|
+
await fake.deliver(result())
|
|
File without changes
|
|
File without changes
|
{kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api/events.py
RENAMED
|
File without changes
|
{kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api/files.py
RENAMED
|
File without changes
|
{kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api/images.py
RENAMED
|
File without changes
|
{kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api/py.typed
RENAMED
|
File without changes
|
{kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api/settings.py
RENAMED
|
File without changes
|
{kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.0}/src/kimi_agent_module_api/tools.py
RENAMED
|
File without changes
|
{kimi_agent_module_api-2.3.0 → kimi_agent_module_api-2.4.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
|
|
File without changes
|