loong-agent 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. loong_agent-0.0.1/PKG-INFO +87 -0
  2. loong_agent-0.0.1/README.md +68 -0
  3. loong_agent-0.0.1/pyproject.toml +50 -0
  4. loong_agent-0.0.1/pyproject.toml.orig +42 -0
  5. loong_agent-0.0.1/src/loong_agent/__init__.py +64 -0
  6. loong_agent-0.0.1/src/loong_agent/client.py +89 -0
  7. loong_agent-0.0.1/src/loong_agent/hooks/__init__.py +14 -0
  8. loong_agent-0.0.1/src/loong_agent/hooks/events.py +57 -0
  9. loong_agent-0.0.1/src/loong_agent/hooks/matcher.py +33 -0
  10. loong_agent-0.0.1/src/loong_agent/hooks/registry.py +124 -0
  11. loong_agent-0.0.1/src/loong_agent/llm/__init__.py +19 -0
  12. loong_agent-0.0.1/src/loong_agent/llm/base.py +61 -0
  13. loong_agent-0.0.1/src/loong_agent/llm/openai_compatible.py +175 -0
  14. loong_agent-0.0.1/src/loong_agent/llm/registry.py +43 -0
  15. loong_agent-0.0.1/src/loong_agent/loop/__init__.py +7 -0
  16. loong_agent-0.0.1/src/loong_agent/loop/context.py +37 -0
  17. loong_agent-0.0.1/src/loong_agent/loop/engine.py +204 -0
  18. loong_agent-0.0.1/src/loong_agent/loop/turn.py +159 -0
  19. loong_agent-0.0.1/src/loong_agent/mcp/__init__.py +15 -0
  20. loong_agent-0.0.1/src/loong_agent/mcp/client.py +91 -0
  21. loong_agent-0.0.1/src/loong_agent/mcp/gateway.py +98 -0
  22. loong_agent-0.0.1/src/loong_agent/mcp/transports/__init__.py +8 -0
  23. loong_agent-0.0.1/src/loong_agent/mcp/transports/base.py +27 -0
  24. loong_agent-0.0.1/src/loong_agent/mcp/transports/http.py +48 -0
  25. loong_agent-0.0.1/src/loong_agent/mcp/transports/sse.py +170 -0
  26. loong_agent-0.0.1/src/loong_agent/mcp/transports/stdio.py +98 -0
  27. loong_agent-0.0.1/src/loong_agent/permissions/__init__.py +11 -0
  28. loong_agent-0.0.1/src/loong_agent/permissions/decision.py +39 -0
  29. loong_agent-0.0.1/src/loong_agent/permissions/policy.py +46 -0
  30. loong_agent-0.0.1/src/loong_agent/plugins/__init__.py +7 -0
  31. loong_agent-0.0.1/src/loong_agent/plugins/lifecycle.py +76 -0
  32. loong_agent-0.0.1/src/loong_agent/plugins/loader.py +106 -0
  33. loong_agent-0.0.1/src/loong_agent/plugins/manifest.py +43 -0
  34. loong_agent-0.0.1/src/loong_agent/skills/__init__.py +7 -0
  35. loong_agent-0.0.1/src/loong_agent/skills/discovery.py +36 -0
  36. loong_agent-0.0.1/src/loong_agent/skills/executor.py +72 -0
  37. loong_agent-0.0.1/src/loong_agent/skills/loader.py +72 -0
  38. loong_agent-0.0.1/src/loong_agent/subagents/__init__.py +7 -0
  39. loong_agent-0.0.1/src/loong_agent/subagents/definition.py +29 -0
  40. loong_agent-0.0.1/src/loong_agent/subagents/orchestrator.py +62 -0
  41. loong_agent-0.0.1/src/loong_agent/subagents/runner.py +47 -0
  42. loong_agent-0.0.1/src/loong_agent/tools/__init__.py +7 -0
  43. loong_agent-0.0.1/src/loong_agent/tools/base.py +131 -0
  44. loong_agent-0.0.1/src/loong_agent/tools/builtin/__init__.py +22 -0
  45. loong_agent-0.0.1/src/loong_agent/tools/builtin/bash.py +41 -0
  46. loong_agent-0.0.1/src/loong_agent/tools/builtin/files.py +113 -0
  47. loong_agent-0.0.1/src/loong_agent/tools/registry.py +59 -0
  48. loong_agent-0.0.1/src/loong_agent/tracing/__init__.py +26 -0
  49. loong_agent-0.0.1/src/loong_agent/tracing/export.py +39 -0
  50. loong_agent-0.0.1/src/loong_agent/tracing/spans.py +21 -0
  51. loong_agent-0.0.1/src/loong_agent/tracing/tracer.py +90 -0
  52. loong_agent-0.0.1/src/loong_agent/types/__init__.py +37 -0
  53. loong_agent-0.0.1/src/loong_agent/types/messages.py +214 -0
  54. loong_agent-0.0.1/src/loong_agent/types/options.py +104 -0
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.3
2
+ Name: loong-agent
3
+ Version: 0.0.1
4
+ Summary: An open-source, model-agnostic autonomous Agent SDK based on the ReAct framework
5
+ Author: loong-agent
6
+ License: MIT
7
+ Requires-Dist: httpx>=0.28.1
8
+ Requires-Dist: opentelemetry-api>=1.44.0
9
+ Requires-Dist: opentelemetry-sdk>=1.44.0
10
+ Requires-Dist: pytest>=9.1.1 ; extra == 'dev'
11
+ Requires-Dist: pytest-asyncio>=1.4.0 ; extra == 'dev'
12
+ Requires-Dist: ruff>=0.16.6 ; extra == 'dev'
13
+ Requires-Dist: mypy>=2.3.1 ; extra == 'dev'
14
+ Requires-Dist: mkdocs>=1.6.1 ; extra == 'docs'
15
+ Requires-Python: >=3.10
16
+ Provides-Extra: dev
17
+ Provides-Extra: docs
18
+ Description-Content-Type: text/markdown
19
+
20
+ # loong-agent
21
+
22
+ Agent framework for all — a fully open-source, model-agnostic, pure-code autonomous AI Agent SDK.
23
+
24
+ Built on the [ReAct](https://arxiv.org/abs/2210.03629) (Reasoning + Acting) framework and inspired by the design of the Claude Agent SDK, it provides a thought-action-observation loop, a tool system, plugins, MCP, Skills, subagents, hooks, permissions, and tracing.
25
+
26
+ ## Features
27
+
28
+ - **Model-agnostic**: works with any OpenAI-compatible API (OpenAI, local LLaMA.cpp / Ollama / vLLM, etc.).
29
+ - **Pure code implementation**: all logic is written in Python source, with no dependency on commercial closed-source binaries.
30
+ - **ReAct loop**: an autonomous execution engine of Think → Act → Observe.
31
+ - **Rich components**: built-in tools, hook system, permission policies, MCP gateway, Skills, subagents, plugins, and OpenTelemetry tracing.
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install -e .
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ```python
42
+ import asyncio
43
+ from loong_agent import FreeAgentOptions, query
44
+ from loong_agent.llm import OpenAICompatibleProvider
45
+
46
+ async def main():
47
+ options = FreeAgentOptions(
48
+ llm=OpenAICompatibleProvider(
49
+ base_url="https://api.openai.com/v1",
50
+ api_key="sk-...",
51
+ model="gpt-4o-mini",
52
+ ),
53
+ allowed_tools=["Read", "Write", "Bash", "Grep", "Glob"],
54
+ )
55
+
56
+ async for message in query("Review the code in src/", options=options):
57
+ if message.type == "assistant":
58
+ print(f"Agent: {message.text}")
59
+ elif message.type == "result":
60
+ print(f"Done! Usage: {message.usage}")
61
+
62
+ asyncio.run(main())
63
+ ```
64
+
65
+ See [`examples/`](examples/) for more examples.
66
+
67
+ ## Project Structure
68
+
69
+ ```
70
+ src/free_agent/
71
+ ├── client.py # FreeAgentClient / query entry point
72
+ ├── loop/ # Agent Loop engine (ReAct loop)
73
+ ├── llm/ # LLM adapter layer (OpenAI-compatible)
74
+ ├── tools/ # Tool system + built-in tools (Read/Write/Edit/Bash/Grep/Glob)
75
+ ├── hooks/ # Hook system
76
+ ├── permissions/ # Permission system
77
+ ├── mcp/ # MCP gateway (stdio / http / in-process)
78
+ ├── skills/ # Skills system (SKILL.md)
79
+ ├── subagents/ # Subagent system
80
+ ├── plugins/ # Plugin system
81
+ ├── tracing/ # OpenTelemetry tracing
82
+ └── types/ # Core types (Message / Options)
83
+ ```
84
+
85
+ ## License
86
+
87
+ MIT
@@ -0,0 +1,68 @@
1
+ # loong-agent
2
+
3
+ Agent framework for all — a fully open-source, model-agnostic, pure-code autonomous AI Agent SDK.
4
+
5
+ Built on the [ReAct](https://arxiv.org/abs/2210.03629) (Reasoning + Acting) framework and inspired by the design of the Claude Agent SDK, it provides a thought-action-observation loop, a tool system, plugins, MCP, Skills, subagents, hooks, permissions, and tracing.
6
+
7
+ ## Features
8
+
9
+ - **Model-agnostic**: works with any OpenAI-compatible API (OpenAI, local LLaMA.cpp / Ollama / vLLM, etc.).
10
+ - **Pure code implementation**: all logic is written in Python source, with no dependency on commercial closed-source binaries.
11
+ - **ReAct loop**: an autonomous execution engine of Think → Act → Observe.
12
+ - **Rich components**: built-in tools, hook system, permission policies, MCP gateway, Skills, subagents, plugins, and OpenTelemetry tracing.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install -e .
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```python
23
+ import asyncio
24
+ from loong_agent import FreeAgentOptions, query
25
+ from loong_agent.llm import OpenAICompatibleProvider
26
+
27
+ async def main():
28
+ options = FreeAgentOptions(
29
+ llm=OpenAICompatibleProvider(
30
+ base_url="https://api.openai.com/v1",
31
+ api_key="sk-...",
32
+ model="gpt-4o-mini",
33
+ ),
34
+ allowed_tools=["Read", "Write", "Bash", "Grep", "Glob"],
35
+ )
36
+
37
+ async for message in query("Review the code in src/", options=options):
38
+ if message.type == "assistant":
39
+ print(f"Agent: {message.text}")
40
+ elif message.type == "result":
41
+ print(f"Done! Usage: {message.usage}")
42
+
43
+ asyncio.run(main())
44
+ ```
45
+
46
+ See [`examples/`](examples/) for more examples.
47
+
48
+ ## Project Structure
49
+
50
+ ```
51
+ src/free_agent/
52
+ ├── client.py # FreeAgentClient / query entry point
53
+ ├── loop/ # Agent Loop engine (ReAct loop)
54
+ ├── llm/ # LLM adapter layer (OpenAI-compatible)
55
+ ├── tools/ # Tool system + built-in tools (Read/Write/Edit/Bash/Grep/Glob)
56
+ ├── hooks/ # Hook system
57
+ ├── permissions/ # Permission system
58
+ ├── mcp/ # MCP gateway (stdio / http / in-process)
59
+ ├── skills/ # Skills system (SKILL.md)
60
+ ├── subagents/ # Subagent system
61
+ ├── plugins/ # Plugin system
62
+ ├── tracing/ # OpenTelemetry tracing
63
+ └── types/ # Core types (Message / Options)
64
+ ```
65
+
66
+ ## License
67
+
68
+ MIT
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "loong-agent"
3
+ version = "0.0.1"
4
+ description = "An open-source, model-agnostic autonomous Agent SDK based on the ReAct framework"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "httpx>=0.28.1",
9
+ "opentelemetry-api>=1.44.0",
10
+ "opentelemetry-sdk>=1.44.0",
11
+ ]
12
+
13
+ [project.license]
14
+ text = "MIT"
15
+
16
+ [[project.authors]]
17
+ name = "loong-agent"
18
+
19
+ [project.optional-dependencies]
20
+ dev = [
21
+ "pytest>=9.1.1",
22
+ "pytest-asyncio>=1.4.0",
23
+ "ruff>=0.16.6",
24
+ "mypy>=2.3.1",
25
+ ]
26
+ docs = ["mkdocs>=1.6.1"]
27
+
28
+ [build-system]
29
+ requires = ["uv_build >= 0.12.5, <0.13.0"]
30
+ build-backend = "uv_build"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["src/loong_agent"]
34
+
35
+ [tool.ruff]
36
+ line-length = 100
37
+ target-version = "py310"
38
+
39
+ [tool.ruff.lint]
40
+ select = [
41
+ "E",
42
+ "F",
43
+ "I",
44
+ "UP",
45
+ "B",
46
+ ]
47
+
48
+ [tool.pytest.ini_options]
49
+ asyncio_mode = "auto"
50
+ testpaths = ["tests"]
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "loong-agent"
3
+ version = "0.0.1"
4
+ description = "An open-source, model-agnostic autonomous Agent SDK based on the ReAct framework"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "loong-agent" }]
9
+ dependencies = [
10
+ "httpx>=0.28.1",
11
+ "opentelemetry-api>=1.44.0",
12
+ "opentelemetry-sdk>=1.44.0",
13
+ ]
14
+
15
+ [project.optional-dependencies]
16
+ dev = [
17
+ "pytest>=9.1.1",
18
+ "pytest-asyncio>=1.4.0",
19
+ "ruff>=0.16.6",
20
+ "mypy>=2.3.1",
21
+ ]
22
+ docs = [
23
+ "mkdocs>=1.6.1",
24
+ ]
25
+
26
+ [build-system]
27
+ requires = ["uv_build >= 0.12.5, <0.13.0"]
28
+ build-backend = "uv_build"
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["src/loong_agent"]
32
+
33
+ [tool.ruff]
34
+ line-length = 100
35
+ target-version = "py310"
36
+
37
+ [tool.ruff.lint]
38
+ select = ["E", "F", "I", "UP", "B"]
39
+
40
+ [tool.pytest.ini_options]
41
+ asyncio_mode = "auto"
42
+ testpaths = ["tests"]
@@ -0,0 +1,64 @@
1
+ """loong — an open-source, model-agnostic autonomous Agent SDK.
2
+
3
+ Quick start::
4
+
5
+ import asyncio
6
+ from free_agent import FreeAgentOptions, query
7
+ from loong_agent.llm import OpenAICompatibleProvider
8
+
9
+ options = FreeAgentOptions(
10
+ llm=OpenAICompatibleProvider(
11
+ base_url="https://api.openai.com/v1",
12
+ api_key="sk-...",
13
+ model="gpt-4o-mini",
14
+ ),
15
+ allowed_tools=["Read", "Write", "Bash"],
16
+ )
17
+
18
+ async def main() -> None:
19
+ async for message in query("Review src/", options=options):
20
+ print(message.type, message.text)
21
+
22
+ asyncio.run(main())
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from loong_agent.client import FreeAgentClient, query
28
+ from loong_agent.loop.engine import AgentLoop
29
+ from loong_agent.subagents.definition import AgentDefinition
30
+ from loong_agent.tools import Tool, tool
31
+ from loong_agent.types import (
32
+ AssistantMessage,
33
+ FreeAgentOptions,
34
+ Message,
35
+ MessageType,
36
+ ResultMessage,
37
+ SystemMessage,
38
+ ToolCall,
39
+ ToolResult,
40
+ Usage,
41
+ UserMessage,
42
+ )
43
+
44
+ __version__ = "0.1.0"
45
+
46
+ __all__ = [
47
+ "query",
48
+ "FreeAgentClient",
49
+ "FreeAgentOptions",
50
+ "AgentLoop",
51
+ "AgentDefinition",
52
+ "Tool",
53
+ "tool",
54
+ "Message",
55
+ "MessageType",
56
+ "ToolCall",
57
+ "ToolResult",
58
+ "Usage",
59
+ "SystemMessage",
60
+ "AssistantMessage",
61
+ "UserMessage",
62
+ "ResultMessage",
63
+ "__version__",
64
+ ]
@@ -0,0 +1,89 @@
1
+ """Public client and convenience entrypoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import AsyncIterator, Optional
7
+
8
+ from loong.loop.engine import AgentLoop
9
+ from loong.types.messages import Message
10
+ from loong.types.options import FreeAgentOptions
11
+
12
+
13
+ async def query(
14
+ prompt: str,
15
+ options: Optional[FreeAgentOptions] = None,
16
+ ) -> AsyncIterator[Message]:
17
+ """Stream the execution of an agent task, yielding each message in turn.
18
+
19
+ Example::
20
+
21
+ async for message in query("Summarize src/", options=options):
22
+ print(message.type, message.text)
23
+ """
24
+ if options is None:
25
+ options = FreeAgentOptions()
26
+ loop = AgentLoop(options)
27
+ try:
28
+ async for message in loop.run(prompt):
29
+ yield message
30
+ finally:
31
+ await loop.aclose()
32
+
33
+
34
+ class FreeAgentClient:
35
+ """Stateful client supporting send/receive streaming and interrupts."""
36
+
37
+ def __init__(self, options: Optional[FreeAgentOptions] = None) -> None:
38
+ self.options = options or FreeAgentOptions()
39
+ self.loop = AgentLoop(self.options)
40
+ self._queue: asyncio.Queue = asyncio.Queue()
41
+ self._task: Optional[asyncio.Task] = None
42
+
43
+ async def send(self, prompt: str) -> None:
44
+ """Start processing ``prompt``; results are read via :meth:`receive`."""
45
+ if self._task is not None and not self._task.done():
46
+ raise RuntimeError("A request is already in progress.")
47
+ self._task = asyncio.create_task(self._run(prompt))
48
+
49
+ async def _run(self, prompt: str) -> None:
50
+ try:
51
+ async for message in self.loop.run(prompt):
52
+ await self._queue.put(message)
53
+ except Exception as exc: # noqa: BLE001
54
+ await self._queue.put(
55
+ Message.result(text=f"Error: {exc}", is_error=True)
56
+ )
57
+ finally:
58
+ await self._queue.put(_SENTINEL)
59
+
60
+ async def receive(self) -> AsyncIterator[Message]:
61
+ """Yield messages produced by the current request until completion."""
62
+ while True:
63
+ message = await self._queue.get()
64
+ if message is _SENTINEL:
65
+ break
66
+ yield message
67
+
68
+ async def interrupt(self) -> None:
69
+ """Cancel the in-flight request."""
70
+ if self._task is not None and not self._task.done():
71
+ self._task.cancel()
72
+ try:
73
+ await self._task
74
+ except asyncio.CancelledError:
75
+ pass
76
+ await self._queue.put(_SENTINEL)
77
+
78
+ async def close(self) -> None:
79
+ await self.loop.aclose()
80
+
81
+
82
+ class _Sentinel:
83
+ pass
84
+
85
+
86
+ _SENTINEL = _Sentinel()
87
+
88
+
89
+ __all__ = ["query", "FreeAgentClient"]
@@ -0,0 +1,14 @@
1
+ """Hook system."""
2
+
3
+ from loong.hooks.events import HookContext, HookEvent, HookOutput
4
+ from loong.hooks.matcher import HookMatcher
5
+ from loong.hooks.registry import Hook, HookRegistry
6
+
7
+ __all__ = [
8
+ "HookEvent",
9
+ "HookContext",
10
+ "HookOutput",
11
+ "HookMatcher",
12
+ "Hook",
13
+ "HookRegistry",
14
+ ]
@@ -0,0 +1,57 @@
1
+ """Hook event definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import Any, Dict, Optional
8
+
9
+
10
+ class HookEvent(str, Enum):
11
+ PRE_TOOL_USE = "PreToolUse"
12
+ POST_TOOL_USE = "PostToolUse"
13
+ SUBAGENT_START = "SubagentStart"
14
+ SUBAGENT_STOP = "SubagentStop"
15
+ SESSION_START = "SessionStart"
16
+ SESSION_END = "SessionEnd"
17
+ IDLE = "Idle"
18
+
19
+
20
+ @dataclass
21
+ class HookContext:
22
+ """Metadata passed to hook callbacks."""
23
+
24
+ session_id: Optional[str] = None
25
+ cwd: Optional[str] = None
26
+ model: Optional[str] = None
27
+ extra: Dict[str, Any] = field(default_factory=dict)
28
+
29
+
30
+ @dataclass
31
+ class HookOutput:
32
+ """Return value of a hook callback.
33
+
34
+ Attributes:
35
+ permission_decision: ``"allow"``, ``"deny"`` or ``"ask"``.
36
+ updated_input: Replacement input for the operation being hooked.
37
+ injected_context: Additional context injected into the conversation.
38
+ output: Optional message to surface to the caller.
39
+ stop_reason: Optional reason string if the hook stops the session.
40
+ """
41
+
42
+ permission_decision: str = "allow"
43
+ updated_input: Optional[Dict[str, Any]] = None
44
+ injected_context: Optional[str] = None
45
+ output: Optional[str] = None
46
+ stop_reason: Optional[str] = None
47
+
48
+ @classmethod
49
+ def allow(cls, **kwargs: Any) -> "HookOutput":
50
+ return cls(permission_decision="allow", **kwargs)
51
+
52
+ @classmethod
53
+ def deny(cls, reason: str = "", **kwargs: Any) -> "HookOutput":
54
+ return cls(permission_decision="deny", output=reason or None, **kwargs)
55
+
56
+
57
+ __all__ = ["HookEvent", "HookContext", "HookOutput"]
@@ -0,0 +1,33 @@
1
+ """Hook matching utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from typing import Optional
8
+
9
+ from loong.hooks.events import HookEvent
10
+
11
+
12
+ @dataclass
13
+ class HookMatcher:
14
+ """Filters whether a hook should fire for a given event/input.
15
+
16
+ ``matcher``, when provided, is a regular expression tested against the
17
+ string form of the hook input (e.g. a tool name).
18
+ """
19
+
20
+ event: HookEvent
21
+ matcher: Optional[str] = None
22
+
23
+ def matches(self, event: HookEvent, target: Optional[str] = None) -> bool:
24
+ if self.event != event:
25
+ return False
26
+ if self.matcher is None:
27
+ return True
28
+ if target is None:
29
+ return False
30
+ return re.search(self.matcher, target) is not None
31
+
32
+
33
+ __all__ = ["HookMatcher"]
@@ -0,0 +1,124 @@
1
+ """Hook registration and dispatch."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, Awaitable, Callable, Dict, List, Optional
8
+
9
+ from loong.hooks.events import HookContext, HookEvent, HookOutput
10
+ from loong.hooks.matcher import HookMatcher
11
+
12
+ HookCallback = Callable[..., Any]
13
+
14
+
15
+ @dataclass
16
+ class Hook:
17
+ """A registered hook callback with an optional matcher."""
18
+
19
+ callback: HookCallback
20
+ matcher: Optional[HookMatcher] = None
21
+
22
+ async def invoke(
23
+ self,
24
+ event: HookEvent,
25
+ input_data: Optional[Dict[str, Any]] = None,
26
+ tool_use_id: Optional[str] = None,
27
+ context: Optional[HookContext] = None,
28
+ ) -> Optional[HookOutput]:
29
+ if self.matcher is not None and not self.matcher.matches(
30
+ event, _target(input_data)
31
+ ):
32
+ return None
33
+
34
+ signature = inspect.signature(self.callback)
35
+ accepts_var_kw = any(
36
+ p.kind == inspect.Parameter.VAR_KEYWORD for p in signature.parameters.values()
37
+ )
38
+ available: Dict[str, Any] = {
39
+ "input_data": input_data or {},
40
+ "tool_use_id": tool_use_id,
41
+ "context": context or HookContext(),
42
+ }
43
+ kwargs: Dict[str, Any] = {
44
+ name: value
45
+ for name, value in available.items()
46
+ if name in signature.parameters or accepts_var_kw
47
+ }
48
+
49
+ result = self.callback(**kwargs)
50
+ if inspect.isawaitable(result):
51
+ result = await result
52
+ if result is None:
53
+ return None
54
+ if isinstance(result, HookOutput):
55
+ return result
56
+ # Allow callbacks to return a plain permission string.
57
+ if isinstance(result, str):
58
+ return HookOutput(permission_decision=result)
59
+ return HookOutput(**result) if isinstance(result, dict) else None
60
+
61
+
62
+ def _target(input_data: Optional[Dict[str, Any]]) -> Optional[str]:
63
+ if not input_data:
64
+ return None
65
+ for key in ("tool_name", "name", "event"):
66
+ if key in input_data:
67
+ return str(input_data[key])
68
+ return None
69
+
70
+
71
+ class HookRegistry:
72
+ """Collects hooks and dispatches events to matching callbacks."""
73
+
74
+ def __init__(self) -> None:
75
+ self._hooks: List[Hook] = []
76
+
77
+ def register(self, callback: HookCallback, matcher: Optional[HookMatcher] = None) -> None:
78
+ self._hooks.append(Hook(callback=callback, matcher=matcher))
79
+
80
+ def register_for_event(
81
+ self,
82
+ event: HookEvent,
83
+ callback: HookCallback,
84
+ matcher: Optional[str] = None,
85
+ ) -> None:
86
+ self._hooks.append(
87
+ Hook(callback=callback, matcher=HookMatcher(event=event, matcher=matcher))
88
+ )
89
+
90
+ def clear(self) -> None:
91
+ self._hooks.clear()
92
+
93
+ async def dispatch(
94
+ self,
95
+ event: HookEvent,
96
+ input_data: Optional[Dict[str, Any]] = None,
97
+ tool_use_id: Optional[str] = None,
98
+ context: Optional[HookContext] = None,
99
+ ) -> List[HookOutput]:
100
+ """Invoke every matching hook and return their outputs."""
101
+ outputs: List[HookOutput] = []
102
+ for hook in self._hooks:
103
+ out = await hook.invoke(event, input_data, tool_use_id, context)
104
+ if out is not None:
105
+ outputs.append(out)
106
+ return outputs
107
+
108
+ @staticmethod
109
+ def from_config(config: dict[str, list[HookCallback]]) -> "HookRegistry":
110
+ """Build a registry from an ``{event_name: [callbacks]}`` mapping."""
111
+ registry = HookRegistry()
112
+ for event_name, callbacks in config.items():
113
+ try:
114
+ event = HookEvent(event_name)
115
+ except ValueError:
116
+ # Unknown events are registered with no matcher and dispatched
117
+ # only when an explicit event filter matches the raw name.
118
+ continue
119
+ for callback in callbacks:
120
+ registry.register_for_event(event, callback)
121
+ return registry
122
+
123
+
124
+ __all__ = ["Hook", "HookRegistry", "HookCallback"]
@@ -0,0 +1,19 @@
1
+ """LLM adapter layer."""
2
+
3
+ from loong.llm.base import ChatResponse, LLMProvider, ToolDefinition
4
+ from loong.llm.openai_compatible import OpenAICompatibleProvider
5
+ from loong.llm.registry import (
6
+ available_providers,
7
+ get_provider,
8
+ register_provider,
9
+ )
10
+
11
+ __all__ = [
12
+ "LLMProvider",
13
+ "ChatResponse",
14
+ "ToolDefinition",
15
+ "OpenAICompatibleProvider",
16
+ "register_provider",
17
+ "get_provider",
18
+ "available_providers",
19
+ ]