borgee-plugin-sdk 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ """Generated BPP wire declarations."""
2
+
3
+ from .frames_generated import BPP_FRAME_DIRECTIONS, BPP_FRAME_TYPES
4
+
5
+ __all__ = ["BPP_FRAME_DIRECTIONS", "BPP_FRAME_TYPES"]
@@ -0,0 +1,272 @@
1
+ # Code generated by cmd/bpp-tsgen; DO NOT EDIT.
2
+ #
3
+ # Source of truth: packages/server-go/internal/bpp/envelope.go
4
+ # Regenerate: cd packages/server-go && go run ./cmd/bpp-tsgen
5
+ # (or: go generate ./internal/bpp/...)
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Literal, NotRequired, TypeAlias, TypedDict
10
+
11
+ BppDirection: TypeAlias = Literal["server_to_plugin", "plugin_to_server"]
12
+
13
+
14
+ class InboundAttachment(TypedDict):
15
+ url: str
16
+ filename: NotRequired[str]
17
+ content_type: str
18
+ size_bytes: NotRequired[int]
19
+
20
+
21
+ class AgentConfigUpdateFrame(TypedDict):
22
+ type: Literal["agent_config_update"]
23
+ cursor: int
24
+ agent_id: str
25
+ schema_version: int
26
+ blob: str
27
+ idempotency_key: str
28
+ created_at: int
29
+
30
+
31
+ class AgentToggleFrame(TypedDict):
32
+ type: Literal["agent_toggle"]
33
+ agent_id: str
34
+ action: str
35
+ reason: str
36
+
37
+
38
+ class InboundMessageFrame(TypedDict):
39
+ type: Literal["inbound_message"]
40
+ cursor: int
41
+ agent_id: str
42
+ channel_id: str
43
+ channel_type: str
44
+ kind: str
45
+ message_id: str
46
+ author_id: str
47
+ author_name: str
48
+ body: str
49
+ content_type: str
50
+ msg_type: str
51
+ attachments: NotRequired[list[InboundAttachment]]
52
+ created_at: int
53
+ emoji: str
54
+ reactor_id: str
55
+ reaction_added: bool
56
+
57
+
58
+ class PermissionDeniedFrame(TypedDict):
59
+ type: Literal["permission_denied"]
60
+ cursor: int
61
+ agent_id: str
62
+ request_id: str
63
+ attempted_action: str
64
+ required_capability: str
65
+ current_scope: str
66
+ denied_at: int
67
+
68
+
69
+ class SemanticActionResultFrame(TypedDict):
70
+ type: Literal["semantic_action_result"]
71
+ nonce: str
72
+ cursor: int
73
+ status: str
74
+ payload: str
75
+ error: str
76
+
77
+
78
+ class SessionResumeAck(TypedDict):
79
+ type: Literal["session.resume_ack"]
80
+ replay_mode: str
81
+ count: int
82
+ high_water: int
83
+ reset: bool
84
+ reason: str
85
+
86
+
87
+ class SessionSummaryFrame(TypedDict):
88
+ type: Literal["session.summary"]
89
+ cursor: int
90
+ missed_count: int
91
+ since_cursor: int
92
+ through_cursor: int
93
+ summary: str
94
+
95
+
96
+ class ConnectFrame(TypedDict):
97
+ type: Literal["connect"]
98
+ plugin_id: str
99
+ token: str
100
+ version: str
101
+ capabilities: str
102
+
103
+
104
+ class AgentRegisterFrame(TypedDict):
105
+ type: Literal["agent_register"]
106
+ plugin_id: str
107
+ agent_ids: list[str]
108
+
109
+
110
+ class RuntimeSchemaAdvertiseFrame(TypedDict):
111
+ type: Literal["runtime_schema_advertise"]
112
+ plugin_id: str
113
+ models: str
114
+ blob_keys: str
115
+ schema_ver: int
116
+
117
+
118
+ class HeartbeatFrame(TypedDict):
119
+ type: Literal["heartbeat"]
120
+ plugin_id: str
121
+ agent_id: str
122
+ status: str
123
+ reason: str
124
+ timestamp: int
125
+
126
+
127
+ class SemanticActionFrame(TypedDict):
128
+ type: Literal["semantic_action"]
129
+ agent_id: str
130
+ action: str
131
+ payload: str
132
+ nonce: str
133
+
134
+
135
+ class ErrorReportFrame(TypedDict):
136
+ type: Literal["error_report"]
137
+ agent_id: str
138
+ kind: str
139
+ detail: str
140
+
141
+
142
+ class AgentConfigAckFrame(TypedDict):
143
+ type: Literal["agent_config_ack"]
144
+ cursor: int
145
+ agent_id: str
146
+ schema_version: int
147
+ idempotency_key: str
148
+ status: str
149
+ reason: str
150
+ applied_at: int
151
+
152
+
153
+ class TaskStartedFrame(TypedDict):
154
+ type: Literal["task_started"]
155
+ task_id: str
156
+ agent_id: str
157
+ channel_id: str
158
+ subject: str
159
+ started_at: int
160
+
161
+
162
+ class TaskFinishedFrame(TypedDict):
163
+ type: Literal["task_finished"]
164
+ task_id: str
165
+ agent_id: str
166
+ channel_id: str
167
+ outcome: str
168
+ reason: str
169
+ finished_at: int
170
+
171
+
172
+ class ColdStartHandshakeFrame(TypedDict):
173
+ type: Literal["cold_start_handshake"]
174
+ plugin_id: str
175
+ agent_id: str
176
+ restart_at: int
177
+ restart_reason: str
178
+
179
+
180
+ class SessionResumeRequest(TypedDict):
181
+ type: Literal["session.resume"]
182
+ replay_mode: str
183
+ since_cursor: int
184
+ latest_n: NotRequired[int]
185
+
186
+
187
+ BppServerToPluginFrame: TypeAlias = (
188
+ AgentConfigUpdateFrame
189
+ | AgentToggleFrame
190
+ | InboundMessageFrame
191
+ | PermissionDeniedFrame
192
+ | SemanticActionResultFrame
193
+ | SessionResumeAck
194
+ | SessionSummaryFrame
195
+ )
196
+
197
+ BppPluginToServerFrame: TypeAlias = (
198
+ ConnectFrame
199
+ | AgentRegisterFrame
200
+ | RuntimeSchemaAdvertiseFrame
201
+ | HeartbeatFrame
202
+ | SemanticActionFrame
203
+ | ErrorReportFrame
204
+ | AgentConfigAckFrame
205
+ | TaskStartedFrame
206
+ | TaskFinishedFrame
207
+ | ColdStartHandshakeFrame
208
+ | SessionResumeRequest
209
+ )
210
+
211
+ BppFrame: TypeAlias = (
212
+ AgentConfigUpdateFrame
213
+ | AgentToggleFrame
214
+ | InboundMessageFrame
215
+ | PermissionDeniedFrame
216
+ | SemanticActionResultFrame
217
+ | SessionResumeAck
218
+ | SessionSummaryFrame
219
+ | ConnectFrame
220
+ | AgentRegisterFrame
221
+ | RuntimeSchemaAdvertiseFrame
222
+ | HeartbeatFrame
223
+ | SemanticActionFrame
224
+ | ErrorReportFrame
225
+ | AgentConfigAckFrame
226
+ | TaskStartedFrame
227
+ | TaskFinishedFrame
228
+ | ColdStartHandshakeFrame
229
+ | SessionResumeRequest
230
+ )
231
+
232
+ BPP_FRAME_TYPES: dict[str, str] = {
233
+ "AgentConfigUpdateFrame": "agent_config_update",
234
+ "AgentToggleFrame": "agent_toggle",
235
+ "InboundMessageFrame": "inbound_message",
236
+ "PermissionDeniedFrame": "permission_denied",
237
+ "SemanticActionResultFrame": "semantic_action_result",
238
+ "SessionResumeAck": "session.resume_ack",
239
+ "SessionSummaryFrame": "session.summary",
240
+ "ConnectFrame": "connect",
241
+ "AgentRegisterFrame": "agent_register",
242
+ "RuntimeSchemaAdvertiseFrame": "runtime_schema_advertise",
243
+ "HeartbeatFrame": "heartbeat",
244
+ "SemanticActionFrame": "semantic_action",
245
+ "ErrorReportFrame": "error_report",
246
+ "AgentConfigAckFrame": "agent_config_ack",
247
+ "TaskStartedFrame": "task_started",
248
+ "TaskFinishedFrame": "task_finished",
249
+ "ColdStartHandshakeFrame": "cold_start_handshake",
250
+ "SessionResumeRequest": "session.resume",
251
+ }
252
+
253
+ BPP_FRAME_DIRECTIONS: dict[str, BppDirection] = {
254
+ "agent_config_update": "server_to_plugin",
255
+ "agent_toggle": "server_to_plugin",
256
+ "inbound_message": "server_to_plugin",
257
+ "permission_denied": "server_to_plugin",
258
+ "semantic_action_result": "server_to_plugin",
259
+ "session.resume_ack": "server_to_plugin",
260
+ "session.summary": "server_to_plugin",
261
+ "connect": "plugin_to_server",
262
+ "agent_register": "plugin_to_server",
263
+ "runtime_schema_advertise": "plugin_to_server",
264
+ "heartbeat": "plugin_to_server",
265
+ "semantic_action": "plugin_to_server",
266
+ "error_report": "plugin_to_server",
267
+ "agent_config_ack": "plugin_to_server",
268
+ "task_started": "plugin_to_server",
269
+ "task_finished": "plugin_to_server",
270
+ "cold_start_handshake": "plugin_to_server",
271
+ "session.resume": "plugin_to_server",
272
+ }
@@ -0,0 +1,400 @@
1
+ """Public async Borgee plugin client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import random
8
+ from collections.abc import AsyncIterator
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, Protocol
11
+
12
+ from ._connection import (
13
+ BppConnection,
14
+ ConfigHandler,
15
+ ConfigObserver,
16
+ ConnectionOptions,
17
+ Delivery,
18
+ ServerRequestHandler,
19
+ )
20
+ from .cursor import CursorStore, MemoryCursorStore
21
+ from .errors import ProtocolError
22
+ from .models import (
23
+ ConnectionState,
24
+ CreatedDM,
25
+ FaultReason,
26
+ ReplayMode,
27
+ SentMessage,
28
+ Task,
29
+ TaskOutcome,
30
+ User,
31
+ task_from_wire,
32
+ user_from_wire,
33
+ )
34
+
35
+
36
+ class _ClientTransport(Protocol):
37
+ @property
38
+ def agent_id(self) -> str: ...
39
+ @property
40
+ def state(self) -> ConnectionState: ...
41
+ async def connect(self) -> None: ...
42
+ async def close(self) -> None: ...
43
+ async def perform(self, operation: str, payload: dict[str, Any]) -> Any: ...
44
+ async def report_task_started(self, task_id: str, channel_id: str, subject: str) -> None: ...
45
+ async def report_task_finished(
46
+ self, task_id: str, channel_id: str, outcome: str, reason: str
47
+ ) -> None: ...
48
+ def send_typing_nowait(self, channel_id: str) -> None: ...
49
+ def deliveries(self) -> AsyncIterator[Delivery]: ...
50
+ def state_changes(self) -> AsyncIterator[ConnectionState]: ...
51
+
52
+
53
+ @dataclass(slots=True)
54
+ class BorgeePluginOptions:
55
+ base_url: str
56
+ api_key: str
57
+ agent_id: str = ""
58
+ plugin_id: str = ""
59
+ cursor_store: CursorStore = field(default_factory=MemoryCursorStore)
60
+ allow_insecure_loopback: bool = False
61
+ replay_mode: ReplayMode = ReplayMode.LATEST_N
62
+ replay_latest_n: int = 200
63
+ config_update_handler: ConfigHandler | None = None
64
+ config_update_observer: ConfigObserver | None = None
65
+ server_request_handler: ServerRequestHandler | None = None
66
+ logger: logging.Logger = field(default_factory=lambda: logging.getLogger("borgee_plugin_sdk"))
67
+ action_timeout: float = 30.0
68
+ config_apply_timeout: float = 5.0
69
+ auto_reconnect: bool = True
70
+ reconnect_base: float = 0.5
71
+ reconnect_max: float = 30.0
72
+ heartbeat_interval: float = 30.0
73
+ random_source: random.Random = field(default_factory=random.Random, repr=False)
74
+
75
+ def __post_init__(self) -> None:
76
+ for name in (
77
+ "action_timeout",
78
+ "config_apply_timeout",
79
+ "reconnect_base",
80
+ "reconnect_max",
81
+ "heartbeat_interval",
82
+ ):
83
+ if getattr(self, name) <= 0:
84
+ raise ValueError(f"{name} must be positive")
85
+
86
+ def to_connection_options(self) -> ConnectionOptions:
87
+ return ConnectionOptions(
88
+ base_url=self.base_url,
89
+ api_key=self.api_key,
90
+ agent_id=self.agent_id,
91
+ plugin_id=self.plugin_id,
92
+ cursor_store=self.cursor_store,
93
+ allow_insecure_loopback=self.allow_insecure_loopback,
94
+ replay_mode=self.replay_mode,
95
+ replay_latest_n=self.replay_latest_n,
96
+ config_update_handler=self.config_update_handler,
97
+ config_update_observer=self.config_update_observer,
98
+ server_request_handler=self.server_request_handler,
99
+ logger=self.logger,
100
+ action_timeout=self.action_timeout,
101
+ config_apply_timeout=self.config_apply_timeout,
102
+ auto_reconnect=self.auto_reconnect,
103
+ reconnect_base=self.reconnect_base,
104
+ reconnect_max=self.reconnect_max,
105
+ heartbeat_interval=self.heartbeat_interval,
106
+ random_source=self.random_source,
107
+ )
108
+
109
+
110
+ class BorgeePluginClient:
111
+ def __init__(self, options: BorgeePluginOptions, transport: _ClientTransport | None = None) -> None:
112
+ self._options = options
113
+ self._transport = transport or BppConnection(options.to_connection_options())
114
+
115
+ @property
116
+ def agent_id(self) -> str:
117
+ return self._transport.agent_id
118
+
119
+ @property
120
+ def state(self) -> ConnectionState:
121
+ return self._transport.state
122
+
123
+ async def connect(self) -> None:
124
+ await self._transport.connect()
125
+
126
+ async def close(self) -> None:
127
+ await self._transport.close()
128
+
129
+ async def __aenter__(self) -> BorgeePluginClient:
130
+ await self.connect()
131
+ return self
132
+
133
+ async def __aexit__(self, exc_type: object, exc: object, traceback: object) -> None:
134
+ await self.close()
135
+
136
+ def deliveries(self) -> AsyncIterator[Delivery]:
137
+ return self._transport.deliveries()
138
+
139
+ def subscribe(self) -> AsyncIterator[Delivery]:
140
+ return self.deliveries()
141
+
142
+ def state_changes(self) -> AsyncIterator[ConnectionState]:
143
+ return self._transport.state_changes()
144
+
145
+ async def send_message(
146
+ self,
147
+ channel_id: str,
148
+ body: str,
149
+ *,
150
+ reply_to_id: str | None = None,
151
+ ) -> SentMessage:
152
+ result = await self._transport.perform(
153
+ "send_message",
154
+ {
155
+ "channel_id": channel_id,
156
+ "content": body,
157
+ "reply_to_id": reply_to_id,
158
+ },
159
+ )
160
+ if not isinstance(result, dict) or not isinstance(result.get("message_id"), str):
161
+ raise ProtocolError("send_message returned an invalid result")
162
+ cursor = result.get("cursor", 0)
163
+ if not isinstance(cursor, int) or isinstance(cursor, bool):
164
+ cursor = 0
165
+ return SentMessage(result["message_id"], cursor)
166
+
167
+ async def edit_message(self, message_id: str, body: str) -> None:
168
+ await self._transport.perform("edit_message", {"message_id": message_id, "content": body})
169
+
170
+ async def delete_message(self, message_id: str) -> None:
171
+ await self._transport.perform("delete_message", {"message_id": message_id})
172
+
173
+ async def react(self, message_id: str, emoji: str) -> None:
174
+ await self._transport.perform("react", {"message_id": message_id, "emoji": emoji, "removed": False})
175
+
176
+ async def unreact(self, message_id: str, emoji: str) -> None:
177
+ await self._transport.perform("react", {"message_id": message_id, "emoji": emoji, "removed": True})
178
+
179
+ async def create_dm(self, user_id: str) -> CreatedDM:
180
+ result = await self._transport.perform("create_dm", {"user_id": user_id})
181
+ if not isinstance(result, dict) or not isinstance(result.get("channel_id"), str):
182
+ raise ProtocolError("create_dm returned an invalid result")
183
+ return CreatedDM(result["channel_id"])
184
+
185
+ async def get_me(self) -> User:
186
+ return user_from_wire(await self._transport.perform("get_me", {}))
187
+
188
+ async def report_task_started(self, task_id: str, channel_id: str, subject: str) -> None:
189
+ await self._transport.report_task_started(task_id, channel_id, subject)
190
+
191
+ async def report_task_finished(
192
+ self,
193
+ task_id: str,
194
+ channel_id: str,
195
+ outcome: TaskOutcome,
196
+ reason: FaultReason | None = None,
197
+ ) -> None:
198
+ await self._transport.report_task_finished(
199
+ task_id,
200
+ channel_id,
201
+ outcome.value,
202
+ reason.value if reason is not None else "",
203
+ )
204
+
205
+ # ─── Task CRUD (BPP semantic actions) ───────────────────
206
+
207
+ async def create_task(
208
+ self,
209
+ channel_id: str,
210
+ title: str,
211
+ *,
212
+ description: str | None = None,
213
+ assignee_id: str | None = None,
214
+ heartbeat_interval_ms: int | None = None,
215
+ heartbeat_prompt: str | None = None,
216
+ ) -> Task:
217
+ payload: dict[str, Any] = {"channel_id": channel_id, "title": title}
218
+ if description is not None:
219
+ payload["description"] = description
220
+ if assignee_id is not None:
221
+ payload["assignee_id"] = assignee_id
222
+ if heartbeat_interval_ms is not None:
223
+ payload["heartbeat_interval_ms"] = heartbeat_interval_ms
224
+ if heartbeat_prompt is not None:
225
+ payload["heartbeat_prompt"] = heartbeat_prompt
226
+ result = await self._transport.perform("create_task", payload)
227
+ return task_from_wire(result)
228
+
229
+ async def list_tasks(
230
+ self,
231
+ *,
232
+ channel_id: str | None = None,
233
+ guild_id: str | None = None,
234
+ ) -> list[Task]:
235
+ if (channel_id is None) == (guild_id is None):
236
+ raise ValueError("list_tasks requires exactly one of channel_id or guild_id")
237
+ payload: dict[str, Any] = {}
238
+ if channel_id is not None:
239
+ payload["channel_id"] = channel_id
240
+ if guild_id is not None:
241
+ payload["guild_id"] = guild_id
242
+ result = await self._transport.perform("list_tasks", payload)
243
+ if not isinstance(result, list):
244
+ raise ProtocolError("list_tasks did not return an array")
245
+ return [task_from_wire(item) for item in result]
246
+
247
+ async def get_task(self, task_id: str) -> Task:
248
+ result = await self._transport.perform("get_task", {"task_id": task_id})
249
+ return task_from_wire(result)
250
+
251
+ async def update_task(
252
+ self,
253
+ task_id: str,
254
+ *,
255
+ status: str | None = None,
256
+ assignee_id: str | None = None,
257
+ title: str | None = None,
258
+ heartbeat_interval_ms: int | None = None,
259
+ heartbeat_prompt: str | None = None,
260
+ ) -> Task:
261
+ if (
262
+ status is None
263
+ and assignee_id is None
264
+ and title is None
265
+ and heartbeat_interval_ms is None
266
+ and heartbeat_prompt is None
267
+ ):
268
+ raise ValueError(
269
+ "update_task requires at least one of: status, assignee_id, title,"
270
+ " heartbeat_interval_ms, heartbeat_prompt"
271
+ )
272
+ payload: dict[str, Any] = {"task_id": task_id}
273
+ if status is not None:
274
+ payload["status"] = status
275
+ if assignee_id is not None:
276
+ payload["assignee_id"] = assignee_id
277
+ if title is not None:
278
+ payload["title"] = title
279
+ if heartbeat_interval_ms is not None:
280
+ payload["heartbeat_interval_ms"] = heartbeat_interval_ms
281
+ if heartbeat_prompt is not None:
282
+ payload["heartbeat_prompt"] = heartbeat_prompt
283
+ result = await self._transport.perform("update_task", payload)
284
+ return task_from_wire(result)
285
+
286
+ # ─── Channel management (BPP semantic actions) ─────────
287
+
288
+ async def list_channels(self) -> list[dict[str, Any]]:
289
+ result = await self._transport.perform("list_channels", {})
290
+ if not isinstance(result, list):
291
+ raise ProtocolError("list_channels did not return an array")
292
+ return result
293
+
294
+ async def create_channel(
295
+ self,
296
+ name: str,
297
+ *,
298
+ topic: str | None = None,
299
+ visibility: str | None = None,
300
+ member_ids: list[str] | None = None,
301
+ ) -> dict[str, Any]:
302
+ payload: dict[str, Any] = {"name": name}
303
+ if topic is not None:
304
+ payload["topic"] = topic
305
+ if visibility is not None:
306
+ payload["visibility"] = visibility
307
+ if member_ids is not None:
308
+ payload["member_ids"] = member_ids
309
+ result = await self._transport.perform("create_channel", payload)
310
+ if not isinstance(result, dict):
311
+ raise ProtocolError("create_channel did not return an object")
312
+ return result
313
+
314
+ async def update_channel(
315
+ self,
316
+ channel_id: str,
317
+ *,
318
+ name: str | None = None,
319
+ visibility: str | None = None,
320
+ archived: bool | None = None,
321
+ ) -> dict[str, Any]:
322
+ payload: dict[str, Any] = {"channel_id": channel_id}
323
+ if name is not None:
324
+ payload["name"] = name
325
+ if visibility is not None:
326
+ payload["visibility"] = visibility
327
+ if archived is not None:
328
+ payload["archived"] = archived
329
+ result = await self._transport.perform("update_channel", payload)
330
+ if not isinstance(result, dict):
331
+ raise ProtocolError("update_channel did not return an object")
332
+ return result
333
+
334
+ async def set_topic(self, channel_id: str, topic: str) -> None:
335
+ await self._transport.perform("set_topic", {"channel_id": channel_id, "topic": topic})
336
+
337
+ async def delete_channel(self, channel_id: str) -> None:
338
+ await self._transport.perform("delete_channel", {"channel_id": channel_id})
339
+
340
+ async def read_history(
341
+ self,
342
+ channel_id: str,
343
+ *,
344
+ limit: int | None = None,
345
+ before: str | None = None,
346
+ after: str | None = None,
347
+ ) -> list[dict[str, Any]]:
348
+ payload: dict[str, Any] = {"channel_id": channel_id}
349
+ if limit is not None:
350
+ payload["limit"] = limit
351
+ if before is not None:
352
+ payload["before"] = before
353
+ if after is not None:
354
+ payload["after"] = after
355
+ result = await self._transport.perform("read_channel_history", payload)
356
+ if not isinstance(result, list):
357
+ raise ProtocolError("read_channel_history did not return an array")
358
+ return result
359
+
360
+ def start_typing(self, channel_id: str) -> _TypingHandle:
361
+ """Begin a typing indicator loop. Call stop() on the handle to end it."""
362
+ handle = _TypingHandle(self._transport, channel_id)
363
+ handle.start()
364
+ return handle
365
+
366
+
367
+ class _TypingHandle:
368
+ """Sends typing frames every 2s until stop() is called."""
369
+
370
+ _INTERVAL = 2.0
371
+
372
+ def __init__(self, transport: _ClientTransport, channel_id: str) -> None:
373
+ self._transport = transport
374
+ self._channel_id = channel_id
375
+ self._task: asyncio.Task[None] | None = None
376
+
377
+ def start(self) -> None:
378
+ if self._task is None or self._task.done():
379
+ self._task = asyncio.create_task(self._loop())
380
+
381
+ async def _loop(self) -> None:
382
+ try:
383
+ while True:
384
+ self._transport.send_typing_nowait(self._channel_id)
385
+ await asyncio.sleep(self._INTERVAL)
386
+ except asyncio.CancelledError:
387
+ pass
388
+
389
+ def stop(self) -> None:
390
+ """Stop emitting typing indicators."""
391
+ if self._task is not None:
392
+ self._task.cancel()
393
+ self._task = None
394
+
395
+ def __del__(self) -> None:
396
+ self.stop()
397
+
398
+
399
+ def create_borgee_plugin(options: BorgeePluginOptions) -> BorgeePluginClient:
400
+ return BorgeePluginClient(options)