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.
- loong_agent-0.0.1/PKG-INFO +87 -0
- loong_agent-0.0.1/README.md +68 -0
- loong_agent-0.0.1/pyproject.toml +50 -0
- loong_agent-0.0.1/pyproject.toml.orig +42 -0
- loong_agent-0.0.1/src/loong_agent/__init__.py +64 -0
- loong_agent-0.0.1/src/loong_agent/client.py +89 -0
- loong_agent-0.0.1/src/loong_agent/hooks/__init__.py +14 -0
- loong_agent-0.0.1/src/loong_agent/hooks/events.py +57 -0
- loong_agent-0.0.1/src/loong_agent/hooks/matcher.py +33 -0
- loong_agent-0.0.1/src/loong_agent/hooks/registry.py +124 -0
- loong_agent-0.0.1/src/loong_agent/llm/__init__.py +19 -0
- loong_agent-0.0.1/src/loong_agent/llm/base.py +61 -0
- loong_agent-0.0.1/src/loong_agent/llm/openai_compatible.py +175 -0
- loong_agent-0.0.1/src/loong_agent/llm/registry.py +43 -0
- loong_agent-0.0.1/src/loong_agent/loop/__init__.py +7 -0
- loong_agent-0.0.1/src/loong_agent/loop/context.py +37 -0
- loong_agent-0.0.1/src/loong_agent/loop/engine.py +204 -0
- loong_agent-0.0.1/src/loong_agent/loop/turn.py +159 -0
- loong_agent-0.0.1/src/loong_agent/mcp/__init__.py +15 -0
- loong_agent-0.0.1/src/loong_agent/mcp/client.py +91 -0
- loong_agent-0.0.1/src/loong_agent/mcp/gateway.py +98 -0
- loong_agent-0.0.1/src/loong_agent/mcp/transports/__init__.py +8 -0
- loong_agent-0.0.1/src/loong_agent/mcp/transports/base.py +27 -0
- loong_agent-0.0.1/src/loong_agent/mcp/transports/http.py +48 -0
- loong_agent-0.0.1/src/loong_agent/mcp/transports/sse.py +170 -0
- loong_agent-0.0.1/src/loong_agent/mcp/transports/stdio.py +98 -0
- loong_agent-0.0.1/src/loong_agent/permissions/__init__.py +11 -0
- loong_agent-0.0.1/src/loong_agent/permissions/decision.py +39 -0
- loong_agent-0.0.1/src/loong_agent/permissions/policy.py +46 -0
- loong_agent-0.0.1/src/loong_agent/plugins/__init__.py +7 -0
- loong_agent-0.0.1/src/loong_agent/plugins/lifecycle.py +76 -0
- loong_agent-0.0.1/src/loong_agent/plugins/loader.py +106 -0
- loong_agent-0.0.1/src/loong_agent/plugins/manifest.py +43 -0
- loong_agent-0.0.1/src/loong_agent/skills/__init__.py +7 -0
- loong_agent-0.0.1/src/loong_agent/skills/discovery.py +36 -0
- loong_agent-0.0.1/src/loong_agent/skills/executor.py +72 -0
- loong_agent-0.0.1/src/loong_agent/skills/loader.py +72 -0
- loong_agent-0.0.1/src/loong_agent/subagents/__init__.py +7 -0
- loong_agent-0.0.1/src/loong_agent/subagents/definition.py +29 -0
- loong_agent-0.0.1/src/loong_agent/subagents/orchestrator.py +62 -0
- loong_agent-0.0.1/src/loong_agent/subagents/runner.py +47 -0
- loong_agent-0.0.1/src/loong_agent/tools/__init__.py +7 -0
- loong_agent-0.0.1/src/loong_agent/tools/base.py +131 -0
- loong_agent-0.0.1/src/loong_agent/tools/builtin/__init__.py +22 -0
- loong_agent-0.0.1/src/loong_agent/tools/builtin/bash.py +41 -0
- loong_agent-0.0.1/src/loong_agent/tools/builtin/files.py +113 -0
- loong_agent-0.0.1/src/loong_agent/tools/registry.py +59 -0
- loong_agent-0.0.1/src/loong_agent/tracing/__init__.py +26 -0
- loong_agent-0.0.1/src/loong_agent/tracing/export.py +39 -0
- loong_agent-0.0.1/src/loong_agent/tracing/spans.py +21 -0
- loong_agent-0.0.1/src/loong_agent/tracing/tracer.py +90 -0
- loong_agent-0.0.1/src/loong_agent/types/__init__.py +37 -0
- loong_agent-0.0.1/src/loong_agent/types/messages.py +214 -0
- 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
|
+
]
|