opencomputer 0.1.0__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.
- opencomputer/__init__.py +3 -0
- opencomputer/agent/__init__.py +1 -0
- opencomputer/agent/compaction.py +245 -0
- opencomputer/agent/config.py +108 -0
- opencomputer/agent/config_store.py +210 -0
- opencomputer/agent/injection.py +60 -0
- opencomputer/agent/loop.py +326 -0
- opencomputer/agent/memory.py +132 -0
- opencomputer/agent/prompt_builder.py +66 -0
- opencomputer/agent/prompts/base.j2 +23 -0
- opencomputer/agent/state.py +251 -0
- opencomputer/agent/step.py +31 -0
- opencomputer/cli.py +483 -0
- opencomputer/doctor.py +216 -0
- opencomputer/gateway/__init__.py +1 -0
- opencomputer/gateway/dispatch.py +89 -0
- opencomputer/gateway/protocol.py +84 -0
- opencomputer/gateway/server.py +77 -0
- opencomputer/gateway/wire_server.py +256 -0
- opencomputer/hooks/__init__.py +1 -0
- opencomputer/hooks/engine.py +79 -0
- opencomputer/hooks/runner.py +42 -0
- opencomputer/mcp/__init__.py +1 -0
- opencomputer/mcp/client.py +208 -0
- opencomputer/plugins/__init__.py +1 -0
- opencomputer/plugins/discovery.py +107 -0
- opencomputer/plugins/loader.py +155 -0
- opencomputer/plugins/registry.py +56 -0
- opencomputer/setup_wizard.py +235 -0
- opencomputer/skills/debug-python-import-error/SKILL.md +58 -0
- opencomputer/tools/__init__.py +1 -0
- opencomputer/tools/bash.py +78 -0
- opencomputer/tools/delegate.py +98 -0
- opencomputer/tools/glob.py +70 -0
- opencomputer/tools/grep.py +117 -0
- opencomputer/tools/read.py +81 -0
- opencomputer/tools/registry.py +69 -0
- opencomputer/tools/skill_manage.py +265 -0
- opencomputer/tools/write.py +58 -0
- opencomputer-0.1.0.dist-info/METADATA +190 -0
- opencomputer-0.1.0.dist-info/RECORD +51 -0
- opencomputer-0.1.0.dist-info/WHEEL +4 -0
- opencomputer-0.1.0.dist-info/entry_points.txt +3 -0
- plugin_sdk/__init__.py +66 -0
- plugin_sdk/channel_contract.py +74 -0
- plugin_sdk/core.py +129 -0
- plugin_sdk/hooks.py +80 -0
- plugin_sdk/injection.py +60 -0
- plugin_sdk/provider_contract.py +95 -0
- plugin_sdk/runtime_context.py +39 -0
- plugin_sdk/tool_contract.py +67 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
opencomputer/__init__.py,sha256=gO2dZNgM6MLxz2pdq32RGQTD2vS27TpPASlsIMAb2TI,75
|
|
2
|
+
opencomputer/cli.py,sha256=Cb7BrTIwGZS1cb-_RAaMJXkXGgkSPoY9ryue5Dco2I0,16277
|
|
3
|
+
opencomputer/doctor.py,sha256=eyQDj1h6GYfwLxljjTlhQoyBDPwtIipOP8pPXyRNTqI,6752
|
|
4
|
+
opencomputer/setup_wizard.py,sha256=g-PFyayuu18s_3HXr3beR1Lbz4BcWg2vXUpVtwwevPI,8370
|
|
5
|
+
opencomputer/agent/__init__.py,sha256=rk4klIVmYuO_obiwCK1bsmrbKLH4VnLGRhi18zgzV90,64
|
|
6
|
+
opencomputer/agent/compaction.py,sha256=1W_3TrAFPSJBk8_26F9dWd2IZ7n5NoyDRCBWx9C3F-c,9374
|
|
7
|
+
opencomputer/agent/config.py,sha256=60cW2Dyv-YPFf6E_ZNQn9xb85RwbgdamvKpleCKNRtk,3150
|
|
8
|
+
opencomputer/agent/config_store.py,sha256=Ep1JCs1-A2fY8a_VfHksiQcMaCiaiU4TGccJgE2RdJQ,7360
|
|
9
|
+
opencomputer/agent/injection.py,sha256=CuNxfKCauIphSWJpO9js_cT6CMBPyWAa1fz4uI_15ig,2101
|
|
10
|
+
opencomputer/agent/loop.py,sha256=b_ip9eqfJ70kpZTmZLF8CHRbMu_fKWSEq5PsJ13ihOk,12356
|
|
11
|
+
opencomputer/agent/memory.py,sha256=DmjpEZxdPHK03Udevicwwzrxhg0xGJcSNbCr0nLLlAQ,4939
|
|
12
|
+
opencomputer/agent/prompt_builder.py,sha256=1_UqCBQx1WYTPITPptj-vyMpNYWpl9SP3goFeI4Tb9A,1825
|
|
13
|
+
opencomputer/agent/state.py,sha256=R1DamH-auT7SqvpopexEuzP5wxOJ3UmmMwgxVnVkvq8,8953
|
|
14
|
+
opencomputer/agent/step.py,sha256=gP0ByNVqeKU-PFAeI1Jlct1iSGXotg5l3Wb7lIxarEQ,823
|
|
15
|
+
opencomputer/agent/prompts/base.j2,sha256=DPR9XNJetxULTLOicUEdDEd7eT6UraAzTRsJJuPExrs,770
|
|
16
|
+
opencomputer/gateway/__init__.py,sha256=SbMLVhBOYJCRpPPXbJuLj5R-jpkv7eJmJtBzXxXlHBo,59
|
|
17
|
+
opencomputer/gateway/dispatch.py,sha256=X5RTO_pYIN60duU1eSFQ8qGeZn2e9qqsLklxpI_z1MY,3351
|
|
18
|
+
opencomputer/gateway/protocol.py,sha256=ccGdrxYOXjN-UVhBp8d1bTZQbUTrnDggDsnR7iPUtIE,2666
|
|
19
|
+
opencomputer/gateway/server.py,sha256=TePY6S2WRPSz8ZQxb1vmbDMSkPObLy-P8tMNGDSCSag,2717
|
|
20
|
+
opencomputer/gateway/wire_server.py,sha256=CeVlF-2WhQ0zTIboFMgjmbNNnD4G330VCYEq6fIoj14,8366
|
|
21
|
+
opencomputer/hooks/__init__.py,sha256=ppxxhfzdZj16u3Wk3RD0RxKvK-sE0ZexY8v1XgwKSLc,50
|
|
22
|
+
opencomputer/hooks/engine.py,sha256=PYs7GYAW8ztFbG8CDOLWEa7oN17oYhzsRge2iJu5pB8,2589
|
|
23
|
+
opencomputer/hooks/runner.py,sha256=_nBl67kBG5Qt7UpLqQvb15-wYL-xye1497EOVdJqK6Y,1108
|
|
24
|
+
opencomputer/mcp/__init__.py,sha256=OidTJFlWyzy7km3k9wDnNXzHrnJzAUcitwY0QcBkd9M,85
|
|
25
|
+
opencomputer/mcp/client.py,sha256=WvA1ovIyQ6A5EA28Wv5d5cr4S-TXIIlW-AcRQUgBceE,7582
|
|
26
|
+
opencomputer/plugins/__init__.py,sha256=gNdUELzcu3CZyS-xhE6hn6utS4Q9l20Nvly4DDx33eI,78
|
|
27
|
+
opencomputer/plugins/discovery.py,sha256=JjKOLRQbSzCKrDcgp_TREHvq_APsNwNak7yBUFpMNd4,3307
|
|
28
|
+
opencomputer/plugins/loader.py,sha256=o_IkpLG6GLayyiyfg0SayuTNS-BRG-wzKaeXkaUKp-Q,5376
|
|
29
|
+
opencomputer/plugins/registry.py,sha256=nOzE1ehcHvpoGrgvyqmOYADe0xY6lZwM60xiF7sTWUY,1992
|
|
30
|
+
opencomputer/skills/debug-python-import-error/SKILL.md,sha256=jBL3sHwtI-4rXbZC92qC5YV7xmP_fkVKGPETIaK5Pvw,2268
|
|
31
|
+
opencomputer/tools/__init__.py,sha256=ZbX8aocncUzMpXQ0sJNZc1Waxu_c0SZInSR3vLMlV5A,80
|
|
32
|
+
opencomputer/tools/bash.py,sha256=ZymfpRnir_9GbUWsTJy4BgmTcZeoDA22cz9mGciZMDE,2770
|
|
33
|
+
opencomputer/tools/delegate.py,sha256=Yn55lUnjlGzjUdoI9RWpHTm0vH_lNMll53l4N0RgW8c,3962
|
|
34
|
+
opencomputer/tools/glob.py,sha256=JXl814MKY0O9hvAfzCqrr38xxXzurQ7Zk7j7fa54L_w,2369
|
|
35
|
+
opencomputer/tools/grep.py,sha256=PCz1xqSNCbU6_guLIwkv_Dj-w1v_zqT7okul4vRmv4k,4081
|
|
36
|
+
opencomputer/tools/read.py,sha256=FRpVndWdb7I09yF5v-GXOE71oqjfe1Vs6dwQbyY2DcU,2855
|
|
37
|
+
opencomputer/tools/registry.py,sha256=97niCcd7cuMtPql4Wilrtf6bv1kayqsedPiyDXd2cco,2190
|
|
38
|
+
opencomputer/tools/skill_manage.py,sha256=CZjBCfkfil8-YAL9jO4AuLRMbfFoQz9NsL4fyjbD9rs,10212
|
|
39
|
+
opencomputer/tools/write.py,sha256=704Qjng-SCj3MlIJu7uul8vSTU1dH_2pkQ9Fsda-Y9g,2010
|
|
40
|
+
plugin_sdk/__init__.py,sha256=UCsA-Of-gdrXdQBXxzFdkYKZ0o64nWcpBnsANCXRCMs,1604
|
|
41
|
+
plugin_sdk/channel_contract.py,sha256=MJ9iaPG7QMTRD3orKkKtKSUtS-kCjEeNc--snl1NrRg,2448
|
|
42
|
+
plugin_sdk/core.py,sha256=mulzBrjhcvPXTvjLrl0Q8sEhnZWZeq2NuG0G8vkrBQw,3683
|
|
43
|
+
plugin_sdk/hooks.py,sha256=nhvjd4qM7T0BZ2G-G5N70VReJ0mHkCyOxKh5TguQ1JY,2589
|
|
44
|
+
plugin_sdk/injection.py,sha256=r2WPZYPcoaYn1gzOLzb9Ni5ZDbNkNg8brFsJySXr9jw,2056
|
|
45
|
+
plugin_sdk/provider_contract.py,sha256=hybjR2AhPRs3rv-1XavaJOP5U4KXkuQ3lt667dOESaE,2716
|
|
46
|
+
plugin_sdk/runtime_context.py,sha256=UOfEsAnIvM5LtX0DVZqw67HvH83KinYQTp8KpAzUUZk,1381
|
|
47
|
+
plugin_sdk/tool_contract.py,sha256=YJ0AsjhG8EiBeTfB5MwHu6nQ8wN16p7vPiBTT9vGas4,1944
|
|
48
|
+
opencomputer-0.1.0.dist-info/METADATA,sha256=TrL-2aVSZ5STLC30LfWRtG00uuml65klT8XXlG3zUpU,6669
|
|
49
|
+
opencomputer-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
50
|
+
opencomputer-0.1.0.dist-info/entry_points.txt,sha256=hvAtUBkItM2YqIhOp3Kh3tOc5tRAM3qAZBn-qG0gzCo,82
|
|
51
|
+
opencomputer-0.1.0.dist-info/RECORD,,
|
plugin_sdk/__init__.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenComputer Plugin SDK — the ONLY public contract for plugins.
|
|
3
|
+
|
|
4
|
+
Third-party plugins must import from `plugin_sdk/*` exclusively. Never
|
|
5
|
+
import from `opencomputer/**` directly — those modules are internal
|
|
6
|
+
and may change without warning. The SDK is versioned and evolves with
|
|
7
|
+
backwards-compatible guarantees across minor releases.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
|
|
12
|
+
from plugin_sdk.channel_contract import BaseChannelAdapter
|
|
13
|
+
from plugin_sdk.core import (
|
|
14
|
+
Message,
|
|
15
|
+
MessageEvent,
|
|
16
|
+
Platform,
|
|
17
|
+
PluginManifest,
|
|
18
|
+
Role,
|
|
19
|
+
SendResult,
|
|
20
|
+
StopReason,
|
|
21
|
+
ToolCall,
|
|
22
|
+
ToolResult,
|
|
23
|
+
)
|
|
24
|
+
from plugin_sdk.hooks import HookContext, HookDecision, HookEvent, HookHandler, HookSpec
|
|
25
|
+
from plugin_sdk.injection import DynamicInjectionProvider, InjectionContext
|
|
26
|
+
from plugin_sdk.provider_contract import (
|
|
27
|
+
BaseProvider,
|
|
28
|
+
ProviderResponse,
|
|
29
|
+
StreamEvent,
|
|
30
|
+
Usage,
|
|
31
|
+
)
|
|
32
|
+
from plugin_sdk.runtime_context import DEFAULT_RUNTIME_CONTEXT, RuntimeContext
|
|
33
|
+
from plugin_sdk.tool_contract import BaseTool, ToolSchema
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"__version__",
|
|
37
|
+
# core types
|
|
38
|
+
"Role",
|
|
39
|
+
"Message",
|
|
40
|
+
"ToolCall",
|
|
41
|
+
"ToolResult",
|
|
42
|
+
"Platform",
|
|
43
|
+
"MessageEvent",
|
|
44
|
+
"SendResult",
|
|
45
|
+
"PluginManifest",
|
|
46
|
+
"StopReason",
|
|
47
|
+
# contracts
|
|
48
|
+
"BaseTool",
|
|
49
|
+
"ToolSchema",
|
|
50
|
+
"BaseProvider",
|
|
51
|
+
"ProviderResponse",
|
|
52
|
+
"StreamEvent",
|
|
53
|
+
"Usage",
|
|
54
|
+
"BaseChannelAdapter",
|
|
55
|
+
# hooks
|
|
56
|
+
"HookEvent",
|
|
57
|
+
"HookContext",
|
|
58
|
+
"HookDecision",
|
|
59
|
+
"HookHandler",
|
|
60
|
+
"HookSpec",
|
|
61
|
+
# runtime + injection
|
|
62
|
+
"RuntimeContext",
|
|
63
|
+
"DEFAULT_RUNTIME_CONTEXT",
|
|
64
|
+
"DynamicInjectionProvider",
|
|
65
|
+
"InjectionContext",
|
|
66
|
+
]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Channel contract — what plugin authors implement to add a messaging channel.
|
|
3
|
+
|
|
4
|
+
A channel adapter translates between a specific messaging platform
|
|
5
|
+
(Telegram, Discord, Slack, ...) and OpenComputer's common MessageEvent
|
|
6
|
+
format. The gateway is platform-agnostic; adapters absorb all the
|
|
7
|
+
platform-specific weirdness.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from collections.abc import Awaitable, Callable
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from plugin_sdk.core import MessageEvent, Platform, SendResult
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BaseChannelAdapter(ABC):
|
|
20
|
+
"""Base class for a messaging channel plugin."""
|
|
21
|
+
|
|
22
|
+
#: The platform this adapter serves.
|
|
23
|
+
platform: Platform
|
|
24
|
+
|
|
25
|
+
#: Max message length this platform accepts (in chars unless noted).
|
|
26
|
+
max_message_length: int = 10_000
|
|
27
|
+
|
|
28
|
+
def __init__(self, config: dict[str, Any]) -> None:
|
|
29
|
+
self.config = config
|
|
30
|
+
self._message_handler: (
|
|
31
|
+
Callable[[MessageEvent], Awaitable[str | None]] | None
|
|
32
|
+
) = None
|
|
33
|
+
|
|
34
|
+
def set_message_handler(
|
|
35
|
+
self, handler: Callable[[MessageEvent], Awaitable[str | None]]
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Called by the gateway to register its inbound handler."""
|
|
38
|
+
self._message_handler = handler
|
|
39
|
+
|
|
40
|
+
async def handle_message(self, event: MessageEvent) -> None:
|
|
41
|
+
"""Adapters call this when an inbound message arrives. Dispatches to the gateway."""
|
|
42
|
+
if self._message_handler is None:
|
|
43
|
+
return
|
|
44
|
+
response = await self._message_handler(event)
|
|
45
|
+
if response:
|
|
46
|
+
await self.send(event.chat_id, response)
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
async def connect(self) -> bool:
|
|
50
|
+
"""Connect to the platform and start listening. Return True on success."""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
async def disconnect(self) -> None:
|
|
55
|
+
"""Stop listening and clean up."""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
async def send(self, chat_id: str, text: str, **kwargs: Any) -> SendResult:
|
|
60
|
+
"""Send a text message to a chat."""
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
async def send_typing(self, chat_id: str) -> None:
|
|
64
|
+
"""Send a typing indicator. Optional — default is a no-op."""
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
async def send_image(
|
|
68
|
+
self, chat_id: str, image_url: str, caption: str = ""
|
|
69
|
+
) -> SendResult:
|
|
70
|
+
"""Send an image. Optional — default raises NotImplementedError."""
|
|
71
|
+
raise NotImplementedError(f"{self.platform} adapter has no image support")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
__all__ = ["BaseChannelAdapter"]
|
plugin_sdk/core.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core public types — the canonical vocabulary plugins use.
|
|
3
|
+
|
|
4
|
+
These types are the STABLE contract. Changes here are breaking changes
|
|
5
|
+
and require a major version bump of plugin_sdk.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Any, Literal
|
|
13
|
+
|
|
14
|
+
# ─── Message / conversation primitives ─────────────────────────────────
|
|
15
|
+
|
|
16
|
+
Role = Literal["system", "user", "assistant", "tool"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class Message:
|
|
21
|
+
"""A single conversation message — canonical form used everywhere internally."""
|
|
22
|
+
|
|
23
|
+
role: Role
|
|
24
|
+
content: str
|
|
25
|
+
tool_call_id: str | None = None
|
|
26
|
+
tool_calls: list[ToolCall] | None = None
|
|
27
|
+
name: str | None = None # for tool messages, the tool name
|
|
28
|
+
reasoning: str | None = None # extended thinking, if supported
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class ToolCall:
|
|
33
|
+
"""A request from the model to invoke a tool."""
|
|
34
|
+
|
|
35
|
+
id: str
|
|
36
|
+
name: str
|
|
37
|
+
arguments: dict[str, Any]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class ToolResult:
|
|
42
|
+
"""The result of executing a tool call."""
|
|
43
|
+
|
|
44
|
+
tool_call_id: str
|
|
45
|
+
content: str
|
|
46
|
+
is_error: bool = False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ─── Platform / channel primitives ─────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Platform(str, Enum):
|
|
53
|
+
"""Supported messaging platforms. Plugins can register new ones."""
|
|
54
|
+
|
|
55
|
+
CLI = "cli"
|
|
56
|
+
TELEGRAM = "telegram"
|
|
57
|
+
DISCORD = "discord"
|
|
58
|
+
SLACK = "slack"
|
|
59
|
+
WHATSAPP = "whatsapp"
|
|
60
|
+
SIGNAL = "signal"
|
|
61
|
+
IMESSAGE = "imessage"
|
|
62
|
+
WEB = "web"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True, slots=True)
|
|
66
|
+
class MessageEvent:
|
|
67
|
+
"""Platform-agnostic inbound message — the common format produced by every adapter."""
|
|
68
|
+
|
|
69
|
+
platform: Platform
|
|
70
|
+
chat_id: str
|
|
71
|
+
user_id: str
|
|
72
|
+
text: str
|
|
73
|
+
timestamp: float # unix timestamp
|
|
74
|
+
attachments: list[str] = field(default_factory=list) # file paths or URLs
|
|
75
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True, slots=True)
|
|
79
|
+
class SendResult:
|
|
80
|
+
"""Outbound delivery result from a channel adapter."""
|
|
81
|
+
|
|
82
|
+
success: bool
|
|
83
|
+
message_id: str | None = None
|
|
84
|
+
error: str | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ─── Plugin manifest ───────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, slots=True)
|
|
91
|
+
class PluginManifest:
|
|
92
|
+
"""Metadata for a plugin — parsed from plugin.json at discovery time."""
|
|
93
|
+
|
|
94
|
+
id: str
|
|
95
|
+
name: str
|
|
96
|
+
version: str
|
|
97
|
+
description: str = ""
|
|
98
|
+
author: str = ""
|
|
99
|
+
homepage: str = ""
|
|
100
|
+
license: str = "MIT"
|
|
101
|
+
kind: Literal["channel", "provider", "tool", "skill", "mixed"] = "mixed"
|
|
102
|
+
entry: str = "" # path to the entry module, relative to plugin root
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ─── Stop reasons ──────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class StopReason(str, Enum):
|
|
109
|
+
"""Why a conversation step ended."""
|
|
110
|
+
|
|
111
|
+
END_TURN = "end_turn" # model produced final response, no more tool calls
|
|
112
|
+
TOOL_USE = "tool_use" # model wants to call tools — loop continues
|
|
113
|
+
MAX_TOKENS = "max_tokens" # hit output limit
|
|
114
|
+
INTERRUPTED = "interrupted" # user cancelled
|
|
115
|
+
BUDGET_EXHAUSTED = "budget_exhausted" # iteration budget spent
|
|
116
|
+
ERROR = "error" # unrecoverable error
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
__all__ = [
|
|
120
|
+
"Role",
|
|
121
|
+
"Message",
|
|
122
|
+
"ToolCall",
|
|
123
|
+
"ToolResult",
|
|
124
|
+
"Platform",
|
|
125
|
+
"MessageEvent",
|
|
126
|
+
"SendResult",
|
|
127
|
+
"PluginManifest",
|
|
128
|
+
"StopReason",
|
|
129
|
+
]
|
plugin_sdk/hooks.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hook primitives — for plugins that want to intercept lifecycle events.
|
|
3
|
+
|
|
4
|
+
Hooks are fire-and-forget event handlers. Critical rule (from kimi-cli):
|
|
5
|
+
post-action hooks MUST NOT block the agent loop. Use async + let the
|
|
6
|
+
loop move on. The hook engine discards exceptions silently (logs them).
|
|
7
|
+
|
|
8
|
+
Available events:
|
|
9
|
+
PreToolUse — fires before any tool runs (can approve/block/modify)
|
|
10
|
+
PostToolUse — fires after any tool runs (log, inspect result)
|
|
11
|
+
Stop — fires when the model stops asking for tools (can force continue)
|
|
12
|
+
SessionStart — fires once when a new conversation begins
|
|
13
|
+
SessionEnd — fires when conversation ends
|
|
14
|
+
UserPromptSubmit — fires when the user submits a message
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import Awaitable, Callable
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from enum import Enum
|
|
22
|
+
from typing import Literal
|
|
23
|
+
|
|
24
|
+
from plugin_sdk.core import Message, ToolCall, ToolResult
|
|
25
|
+
from plugin_sdk.runtime_context import RuntimeContext
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class HookEvent(str, Enum):
|
|
29
|
+
PRE_TOOL_USE = "PreToolUse"
|
|
30
|
+
POST_TOOL_USE = "PostToolUse"
|
|
31
|
+
STOP = "Stop"
|
|
32
|
+
SESSION_START = "SessionStart"
|
|
33
|
+
SESSION_END = "SessionEnd"
|
|
34
|
+
USER_PROMPT_SUBMIT = "UserPromptSubmit"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class HookContext:
|
|
39
|
+
"""Data passed to every hook invocation. Read-only."""
|
|
40
|
+
|
|
41
|
+
event: HookEvent
|
|
42
|
+
session_id: str
|
|
43
|
+
tool_call: ToolCall | None = None
|
|
44
|
+
tool_result: ToolResult | None = None
|
|
45
|
+
message: Message | None = None
|
|
46
|
+
#: Runtime flags for this invocation (plan_mode, yolo_mode, custom).
|
|
47
|
+
#: None for backwards compatibility with hooks written before this field.
|
|
48
|
+
runtime: RuntimeContext | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class HookDecision:
|
|
53
|
+
"""A hook's response. PreToolUse hooks use `decision` to approve/block."""
|
|
54
|
+
|
|
55
|
+
decision: Literal["approve", "block", "pass"] = "pass"
|
|
56
|
+
reason: str = ""
|
|
57
|
+
modified_message: str = "" # if set, injected as a system reminder
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Hook handler is an async callable: (ctx) -> HookDecision or None (= "pass")
|
|
61
|
+
HookHandler = Callable[[HookContext], Awaitable[HookDecision | None]]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True, slots=True)
|
|
65
|
+
class HookSpec:
|
|
66
|
+
"""What plugins register — one event + one handler + an optional matcher."""
|
|
67
|
+
|
|
68
|
+
event: HookEvent
|
|
69
|
+
handler: HookHandler
|
|
70
|
+
matcher: str | None = None # regex over tool names for PreToolUse/PostToolUse
|
|
71
|
+
fire_and_forget: bool = True # true for post-action hooks
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
"HookEvent",
|
|
76
|
+
"HookContext",
|
|
77
|
+
"HookDecision",
|
|
78
|
+
"HookHandler",
|
|
79
|
+
"HookSpec",
|
|
80
|
+
]
|
plugin_sdk/injection.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dynamic injection providers — cross-cutting system-prompt modifiers.
|
|
3
|
+
|
|
4
|
+
A provider declares a piece of text to inject into the system prompt when
|
|
5
|
+
certain runtime conditions apply (e.g. plan mode active). The agent loop
|
|
6
|
+
queries all registered providers at the start of each turn; whichever
|
|
7
|
+
return non-empty strings get appended to the system prompt.
|
|
8
|
+
|
|
9
|
+
This is kimi-cli's pattern — keeps cross-cutting concerns (plan mode, yolo
|
|
10
|
+
mode, custom modes) out of the main loop as if-branches.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
from plugin_sdk.core import Message
|
|
19
|
+
from plugin_sdk.runtime_context import RuntimeContext
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class InjectionContext:
|
|
24
|
+
"""Read-only snapshot passed to each provider's collect() call."""
|
|
25
|
+
|
|
26
|
+
#: Full message history so far (same list the LLM will see this turn).
|
|
27
|
+
messages: tuple[Message, ...]
|
|
28
|
+
#: Per-invocation flags (plan_mode, yolo_mode, etc.).
|
|
29
|
+
runtime: RuntimeContext
|
|
30
|
+
#: Session id — useful for session-scoped caches or per-chat behaviors.
|
|
31
|
+
session_id: str = ""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DynamicInjectionProvider(ABC):
|
|
35
|
+
"""Base class for providers that inject text into the system prompt.
|
|
36
|
+
|
|
37
|
+
Implement `collect()`. Return a string (the injection) or None/empty
|
|
38
|
+
(this provider is not applicable this turn).
|
|
39
|
+
|
|
40
|
+
`priority` orders providers in the final prompt — lower first.
|
|
41
|
+
`provider_id` must be unique per registration; it's also used for
|
|
42
|
+
deterministic ordering when two providers share a priority.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
#: Lower runs first. Plan mode is 10, yolo is 20, user-added modes 50+.
|
|
46
|
+
priority: int = 100
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def provider_id(self) -> str:
|
|
51
|
+
"""Unique id per provider. Used for dedup + ordering stability."""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def collect(self, ctx: InjectionContext) -> str | None:
|
|
56
|
+
"""Return injection text or None if this provider doesn't apply."""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
__all__ = ["DynamicInjectionProvider", "InjectionContext"]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provider contract — what plugin authors implement to add an LLM provider.
|
|
3
|
+
|
|
4
|
+
Providers wrap model APIs (Anthropic, OpenAI, OpenRouter, etc.) behind a
|
|
5
|
+
single interface the agent loop depends on. The agent never imports
|
|
6
|
+
anthropic/openai SDKs directly — it only uses BaseProvider.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from collections.abc import AsyncIterator
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Literal
|
|
15
|
+
|
|
16
|
+
from plugin_sdk.core import Message
|
|
17
|
+
from plugin_sdk.tool_contract import ToolSchema
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class Usage:
|
|
22
|
+
"""Token counts from a single LLM call."""
|
|
23
|
+
|
|
24
|
+
input_tokens: int = 0
|
|
25
|
+
output_tokens: int = 0
|
|
26
|
+
cache_read_tokens: int = 0
|
|
27
|
+
cache_write_tokens: int = 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class ProviderResponse:
|
|
32
|
+
"""The result of calling `provider.complete(...)`."""
|
|
33
|
+
|
|
34
|
+
message: Message # the assistant message, possibly containing tool_calls
|
|
35
|
+
stop_reason: str # "end_turn" | "tool_use" | "max_tokens" | ...
|
|
36
|
+
usage: Usage
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class StreamEvent:
|
|
41
|
+
"""One event emitted by `provider.stream_complete()`.
|
|
42
|
+
|
|
43
|
+
Types:
|
|
44
|
+
- "text_delta": incremental text chunk (`text` field)
|
|
45
|
+
- "tool_call": full tool call has been assembled (`tool_call` field)
|
|
46
|
+
- "done": streaming finished (`response` field carries the final ProviderResponse)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
kind: Literal["text_delta", "tool_call", "done"]
|
|
50
|
+
text: str = ""
|
|
51
|
+
response: ProviderResponse | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class BaseProvider(ABC):
|
|
55
|
+
"""Base class for an LLM provider plugin."""
|
|
56
|
+
|
|
57
|
+
name: str = ""
|
|
58
|
+
default_model: str = ""
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
async def complete(
|
|
62
|
+
self,
|
|
63
|
+
*,
|
|
64
|
+
model: str,
|
|
65
|
+
messages: list[Message],
|
|
66
|
+
system: str = "",
|
|
67
|
+
tools: list[ToolSchema] | None = None,
|
|
68
|
+
max_tokens: int = 4096,
|
|
69
|
+
temperature: float = 1.0,
|
|
70
|
+
stream: bool = False,
|
|
71
|
+
) -> ProviderResponse:
|
|
72
|
+
"""Send messages to the provider, return a single ProviderResponse."""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
async def stream_complete(
|
|
77
|
+
self,
|
|
78
|
+
*,
|
|
79
|
+
model: str,
|
|
80
|
+
messages: list[Message],
|
|
81
|
+
system: str = "",
|
|
82
|
+
tools: list[ToolSchema] | None = None,
|
|
83
|
+
max_tokens: int = 4096,
|
|
84
|
+
temperature: float = 1.0,
|
|
85
|
+
) -> AsyncIterator[StreamEvent]:
|
|
86
|
+
"""Stream the response.
|
|
87
|
+
|
|
88
|
+
Yields StreamEvent objects in order. Final event has kind="done"
|
|
89
|
+
and carries the complete ProviderResponse (including aggregated text
|
|
90
|
+
and any tool calls). Text chunks arrive as kind="text_delta".
|
|
91
|
+
"""
|
|
92
|
+
...
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
__all__ = ["BaseProvider", "ProviderResponse", "Usage", "StreamEvent"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RuntimeContext — per-turn flags passed through the agent loop.
|
|
3
|
+
|
|
4
|
+
The CLI / caller builds this once per invocation. The agent loop passes it
|
|
5
|
+
to InjectionProviders (so they can decide whether to fire) and to Hooks
|
|
6
|
+
(so they can decide whether to block). `delegate` propagates it to
|
|
7
|
+
subagents, so modes like `--plan` apply to the whole subagent tree.
|
|
8
|
+
|
|
9
|
+
Frozen dataclass — safe to share across tasks / threads.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class RuntimeContext:
|
|
20
|
+
"""Flags that cross-cutting modes read. Immutable per invocation."""
|
|
21
|
+
|
|
22
|
+
#: Plan mode — agent should describe what it would do without executing
|
|
23
|
+
#: destructive tools. Enforced both via injection (prompt) and hook (hard-block).
|
|
24
|
+
plan_mode: bool = False
|
|
25
|
+
|
|
26
|
+
#: Yolo mode — auto-approve dangerous operations. Mutually exclusive with plan_mode
|
|
27
|
+
#: (we enforce this in the CLI; if both set, plan_mode wins).
|
|
28
|
+
yolo_mode: bool = False
|
|
29
|
+
|
|
30
|
+
#: Escape hatch for third-party plugins to add their own modes without
|
|
31
|
+
#: forcing an SDK version bump.
|
|
32
|
+
custom: dict[str, Any] = field(default_factory=dict)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
#: A sentinel "no flags" default — useful when callers don't care about modes.
|
|
36
|
+
DEFAULT_RUNTIME_CONTEXT = RuntimeContext()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__all__ = ["RuntimeContext", "DEFAULT_RUNTIME_CONTEXT"]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool contract — what plugin authors implement to add a new tool.
|
|
3
|
+
|
|
4
|
+
A tool is any callable the agent can invoke: Read, Write, Bash, etc.
|
|
5
|
+
Plugins can add new ones by subclassing `BaseTool` and registering
|
|
6
|
+
via `register_plugin(..., tools=[MyTool])`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from plugin_sdk.core import ToolCall, ToolResult
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class ToolSchema:
|
|
20
|
+
"""OpenAI-compatible JSON schema for a tool."""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
description: str
|
|
24
|
+
parameters: dict[str, Any] # JSON Schema object
|
|
25
|
+
|
|
26
|
+
def to_openai_format(self) -> dict[str, Any]:
|
|
27
|
+
"""Convert to the dict format the OpenAI API expects."""
|
|
28
|
+
return {
|
|
29
|
+
"type": "function",
|
|
30
|
+
"function": {
|
|
31
|
+
"name": self.name,
|
|
32
|
+
"description": self.description,
|
|
33
|
+
"parameters": self.parameters,
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def to_anthropic_format(self) -> dict[str, Any]:
|
|
38
|
+
"""Convert to the dict format the Anthropic API expects."""
|
|
39
|
+
return {
|
|
40
|
+
"name": self.name,
|
|
41
|
+
"description": self.description,
|
|
42
|
+
"input_schema": self.parameters,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class BaseTool(ABC):
|
|
47
|
+
"""Base class for a tool. Subclass and implement `schema` + `execute`."""
|
|
48
|
+
|
|
49
|
+
#: Whether this tool is safe to run in parallel with other parallel-safe tools.
|
|
50
|
+
parallel_safe: bool = False
|
|
51
|
+
|
|
52
|
+
#: Maximum size of the result string (longer is truncated with a notice).
|
|
53
|
+
max_result_size: int = 100_000
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def schema(self) -> ToolSchema:
|
|
58
|
+
"""Return the JSON schema describing this tool's input."""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
async def execute(self, call: ToolCall) -> ToolResult:
|
|
63
|
+
"""Actually run the tool. Must handle its own errors — never raise."""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
__all__ = ["ToolSchema", "BaseTool"]
|