aury-agent 0.0.4__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.
- aury/__init__.py +2 -0
- aury/agents/__init__.py +55 -0
- aury/agents/a2a/__init__.py +168 -0
- aury/agents/backends/__init__.py +196 -0
- aury/agents/backends/artifact/__init__.py +9 -0
- aury/agents/backends/artifact/memory.py +130 -0
- aury/agents/backends/artifact/types.py +133 -0
- aury/agents/backends/code/__init__.py +65 -0
- aury/agents/backends/file/__init__.py +11 -0
- aury/agents/backends/file/local.py +66 -0
- aury/agents/backends/file/types.py +40 -0
- aury/agents/backends/invocation/__init__.py +8 -0
- aury/agents/backends/invocation/memory.py +81 -0
- aury/agents/backends/invocation/types.py +110 -0
- aury/agents/backends/memory/__init__.py +8 -0
- aury/agents/backends/memory/memory.py +179 -0
- aury/agents/backends/memory/types.py +136 -0
- aury/agents/backends/message/__init__.py +9 -0
- aury/agents/backends/message/memory.py +122 -0
- aury/agents/backends/message/types.py +124 -0
- aury/agents/backends/sandbox.py +275 -0
- aury/agents/backends/session/__init__.py +8 -0
- aury/agents/backends/session/memory.py +93 -0
- aury/agents/backends/session/types.py +124 -0
- aury/agents/backends/shell/__init__.py +11 -0
- aury/agents/backends/shell/local.py +110 -0
- aury/agents/backends/shell/types.py +55 -0
- aury/agents/backends/shell.py +209 -0
- aury/agents/backends/snapshot/__init__.py +19 -0
- aury/agents/backends/snapshot/git.py +95 -0
- aury/agents/backends/snapshot/hybrid.py +125 -0
- aury/agents/backends/snapshot/memory.py +86 -0
- aury/agents/backends/snapshot/types.py +59 -0
- aury/agents/backends/state/__init__.py +29 -0
- aury/agents/backends/state/composite.py +49 -0
- aury/agents/backends/state/file.py +57 -0
- aury/agents/backends/state/memory.py +52 -0
- aury/agents/backends/state/sqlite.py +262 -0
- aury/agents/backends/state/types.py +178 -0
- aury/agents/backends/subagent/__init__.py +165 -0
- aury/agents/cli/__init__.py +41 -0
- aury/agents/cli/chat.py +239 -0
- aury/agents/cli/config.py +236 -0
- aury/agents/cli/extensions.py +460 -0
- aury/agents/cli/main.py +189 -0
- aury/agents/cli/session.py +337 -0
- aury/agents/cli/workflow.py +276 -0
- aury/agents/context_providers/__init__.py +66 -0
- aury/agents/context_providers/artifact.py +299 -0
- aury/agents/context_providers/base.py +177 -0
- aury/agents/context_providers/memory.py +70 -0
- aury/agents/context_providers/message.py +130 -0
- aury/agents/context_providers/skill.py +50 -0
- aury/agents/context_providers/subagent.py +46 -0
- aury/agents/context_providers/tool.py +68 -0
- aury/agents/core/__init__.py +83 -0
- aury/agents/core/base.py +573 -0
- aury/agents/core/context.py +797 -0
- aury/agents/core/context_builder.py +303 -0
- aury/agents/core/event_bus/__init__.py +15 -0
- aury/agents/core/event_bus/bus.py +203 -0
- aury/agents/core/factory.py +169 -0
- aury/agents/core/isolator.py +97 -0
- aury/agents/core/logging.py +95 -0
- aury/agents/core/parallel.py +194 -0
- aury/agents/core/runner.py +139 -0
- aury/agents/core/services/__init__.py +5 -0
- aury/agents/core/services/file_session.py +144 -0
- aury/agents/core/services/message.py +53 -0
- aury/agents/core/services/session.py +53 -0
- aury/agents/core/signals.py +109 -0
- aury/agents/core/state.py +363 -0
- aury/agents/core/types/__init__.py +107 -0
- aury/agents/core/types/action.py +176 -0
- aury/agents/core/types/artifact.py +135 -0
- aury/agents/core/types/block.py +736 -0
- aury/agents/core/types/message.py +350 -0
- aury/agents/core/types/recall.py +144 -0
- aury/agents/core/types/session.py +257 -0
- aury/agents/core/types/subagent.py +154 -0
- aury/agents/core/types/tool.py +205 -0
- aury/agents/eval/__init__.py +331 -0
- aury/agents/hitl/__init__.py +57 -0
- aury/agents/hitl/ask_user.py +242 -0
- aury/agents/hitl/compaction.py +230 -0
- aury/agents/hitl/exceptions.py +87 -0
- aury/agents/hitl/permission.py +617 -0
- aury/agents/hitl/revert.py +216 -0
- aury/agents/llm/__init__.py +31 -0
- aury/agents/llm/adapter.py +367 -0
- aury/agents/llm/openai.py +294 -0
- aury/agents/llm/provider.py +476 -0
- aury/agents/mcp/__init__.py +153 -0
- aury/agents/memory/__init__.py +46 -0
- aury/agents/memory/compaction.py +394 -0
- aury/agents/memory/manager.py +465 -0
- aury/agents/memory/processor.py +177 -0
- aury/agents/memory/store.py +187 -0
- aury/agents/memory/types.py +137 -0
- aury/agents/messages/__init__.py +40 -0
- aury/agents/messages/config.py +47 -0
- aury/agents/messages/raw_store.py +224 -0
- aury/agents/messages/store.py +118 -0
- aury/agents/messages/types.py +88 -0
- aury/agents/middleware/__init__.py +31 -0
- aury/agents/middleware/base.py +341 -0
- aury/agents/middleware/chain.py +342 -0
- aury/agents/middleware/message.py +129 -0
- aury/agents/middleware/message_container.py +126 -0
- aury/agents/middleware/raw_message.py +153 -0
- aury/agents/middleware/truncation.py +139 -0
- aury/agents/middleware/types.py +81 -0
- aury/agents/plugin.py +162 -0
- aury/agents/react/__init__.py +4 -0
- aury/agents/react/agent.py +1923 -0
- aury/agents/sandbox/__init__.py +23 -0
- aury/agents/sandbox/local.py +239 -0
- aury/agents/sandbox/remote.py +200 -0
- aury/agents/sandbox/types.py +115 -0
- aury/agents/skill/__init__.py +16 -0
- aury/agents/skill/loader.py +180 -0
- aury/agents/skill/types.py +83 -0
- aury/agents/tool/__init__.py +39 -0
- aury/agents/tool/builtin/__init__.py +23 -0
- aury/agents/tool/builtin/ask_user.py +155 -0
- aury/agents/tool/builtin/bash.py +107 -0
- aury/agents/tool/builtin/delegate.py +726 -0
- aury/agents/tool/builtin/edit.py +121 -0
- aury/agents/tool/builtin/plan.py +277 -0
- aury/agents/tool/builtin/read.py +91 -0
- aury/agents/tool/builtin/thinking.py +111 -0
- aury/agents/tool/builtin/yield_result.py +130 -0
- aury/agents/tool/decorator.py +252 -0
- aury/agents/tool/set.py +204 -0
- aury/agents/usage/__init__.py +12 -0
- aury/agents/usage/tracker.py +236 -0
- aury/agents/workflow/__init__.py +85 -0
- aury/agents/workflow/adapter.py +268 -0
- aury/agents/workflow/dag.py +116 -0
- aury/agents/workflow/dsl.py +575 -0
- aury/agents/workflow/executor.py +659 -0
- aury/agents/workflow/expression.py +136 -0
- aury/agents/workflow/parser.py +182 -0
- aury/agents/workflow/state.py +145 -0
- aury/agents/workflow/types.py +86 -0
- aury_agent-0.0.4.dist-info/METADATA +90 -0
- aury_agent-0.0.4.dist-info/RECORD +149 -0
- aury_agent-0.0.4.dist-info/WHEEL +4 -0
- aury_agent-0.0.4.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""SkillLoader - Load skills from filesystem."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from .types import Skill
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SkillLoader:
|
|
15
|
+
"""Load and manage skills from the filesystem.
|
|
16
|
+
|
|
17
|
+
Skills are loaded from SKILL.md files with priority:
|
|
18
|
+
1. project/ - Project-level skills (.aury/skills/)
|
|
19
|
+
2. user/ - User-level skills (~/.aury/skills/)
|
|
20
|
+
3. system/ - Built-in skills (framework provided)
|
|
21
|
+
|
|
22
|
+
Same-named skills: higher priority overrides lower.
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
loader = SkillLoader()
|
|
26
|
+
await loader.load_from_directory(Path(".aury/skills"), source="project")
|
|
27
|
+
|
|
28
|
+
skill = loader.get("code-review")
|
|
29
|
+
all_skills = loader.list_all()
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
self._skills: dict[str, Skill] = {}
|
|
34
|
+
|
|
35
|
+
async def load_from_directory(
|
|
36
|
+
self,
|
|
37
|
+
directory: Path,
|
|
38
|
+
source: Literal["user", "project", "system"] = "user",
|
|
39
|
+
) -> list[Skill]:
|
|
40
|
+
"""Load skills from a directory.
|
|
41
|
+
|
|
42
|
+
Scans for SKILL.md files and loads them.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
directory: Directory to scan
|
|
46
|
+
source: Source identifier for loaded skills
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
List of loaded skills
|
|
50
|
+
"""
|
|
51
|
+
loaded: list[Skill] = []
|
|
52
|
+
|
|
53
|
+
if not directory.exists():
|
|
54
|
+
return loaded
|
|
55
|
+
|
|
56
|
+
for skill_md in directory.rglob("SKILL.md"):
|
|
57
|
+
try:
|
|
58
|
+
skill = await self._load_skill(skill_md, source)
|
|
59
|
+
if skill:
|
|
60
|
+
if skill.name not in self._skills:
|
|
61
|
+
self._skills[skill.name] = skill
|
|
62
|
+
loaded.append(skill)
|
|
63
|
+
except Exception:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
return loaded
|
|
67
|
+
|
|
68
|
+
async def _load_skill(
|
|
69
|
+
self,
|
|
70
|
+
path: Path,
|
|
71
|
+
source: Literal["user", "project", "system"],
|
|
72
|
+
) -> Skill | None:
|
|
73
|
+
"""Load a single skill from SKILL.md."""
|
|
74
|
+
def _parse() -> tuple[dict[str, Any], str] | None:
|
|
75
|
+
text = path.read_text(encoding="utf-8")
|
|
76
|
+
|
|
77
|
+
if not text.startswith("---"):
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
parts = text.split("---", 2)
|
|
81
|
+
if len(parts) < 3:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
frontmatter = yaml.safe_load(parts[1])
|
|
85
|
+
if not frontmatter:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
return frontmatter, parts[2].strip()
|
|
89
|
+
|
|
90
|
+
loop = asyncio.get_event_loop()
|
|
91
|
+
result = await loop.run_in_executor(None, _parse)
|
|
92
|
+
|
|
93
|
+
if not result:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
frontmatter, body = result
|
|
97
|
+
|
|
98
|
+
name = frontmatter.get("name")
|
|
99
|
+
description = frontmatter.get("description")
|
|
100
|
+
|
|
101
|
+
if not name or not description:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
if not re.match(r"^[a-z][a-z0-9-]*$", name) or len(name) > 64:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
allowed_tools = None
|
|
108
|
+
if "allowed_tools" in frontmatter:
|
|
109
|
+
tools_str = frontmatter["allowed_tools"]
|
|
110
|
+
if isinstance(tools_str, str):
|
|
111
|
+
allowed_tools = tools_str.split()
|
|
112
|
+
elif isinstance(tools_str, list):
|
|
113
|
+
allowed_tools = tools_str
|
|
114
|
+
|
|
115
|
+
return Skill(
|
|
116
|
+
name=name,
|
|
117
|
+
description=description.strip(),
|
|
118
|
+
path=path,
|
|
119
|
+
source=source,
|
|
120
|
+
license=frontmatter.get("license"),
|
|
121
|
+
allowed_tools=allowed_tools,
|
|
122
|
+
metadata={k: v for k, v in frontmatter.items()
|
|
123
|
+
if k not in ("name", "description", "license", "allowed_tools")},
|
|
124
|
+
_content=body,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def get(self, name: str) -> Skill | None:
|
|
128
|
+
"""Get skill by name."""
|
|
129
|
+
return self._skills.get(name)
|
|
130
|
+
|
|
131
|
+
def list_all(self) -> list[Skill]:
|
|
132
|
+
"""Get all registered skills."""
|
|
133
|
+
return list(self._skills.values())
|
|
134
|
+
|
|
135
|
+
def match(self, query: str) -> list[Skill]:
|
|
136
|
+
"""Fuzzy match skills based on description.
|
|
137
|
+
|
|
138
|
+
Simple keyword matching - can be enhanced with embeddings.
|
|
139
|
+
"""
|
|
140
|
+
query_lower = query.lower()
|
|
141
|
+
matches = []
|
|
142
|
+
|
|
143
|
+
for skill in self._skills.values():
|
|
144
|
+
desc_lower = skill.description.lower()
|
|
145
|
+
name_lower = skill.name.lower()
|
|
146
|
+
|
|
147
|
+
query_words = query_lower.split()
|
|
148
|
+
if any(word in desc_lower or word in name_lower for word in query_words):
|
|
149
|
+
matches.append(skill)
|
|
150
|
+
|
|
151
|
+
return matches
|
|
152
|
+
|
|
153
|
+
def format_for_prompt(self) -> str:
|
|
154
|
+
"""Format skills for system prompt injection.
|
|
155
|
+
|
|
156
|
+
Returns a markdown-formatted list of available skills.
|
|
157
|
+
"""
|
|
158
|
+
if not self._skills:
|
|
159
|
+
return ""
|
|
160
|
+
|
|
161
|
+
lines = ["## Available Skills\n"]
|
|
162
|
+
|
|
163
|
+
for skill in sorted(self._skills.values(), key=lambda s: s.name):
|
|
164
|
+
desc_first_line = skill.description.split("\n")[0].strip()
|
|
165
|
+
lines.append(f"- **{skill.name}**: {desc_first_line}")
|
|
166
|
+
lines.append(f" Path: `{skill.path.parent}`")
|
|
167
|
+
|
|
168
|
+
if skill.allowed_tools:
|
|
169
|
+
lines.append(f" Tools: {', '.join(skill.allowed_tools)}")
|
|
170
|
+
|
|
171
|
+
lines.append("")
|
|
172
|
+
|
|
173
|
+
return "\n".join(lines)
|
|
174
|
+
|
|
175
|
+
def clear(self) -> None:
|
|
176
|
+
"""Clear all loaded skills."""
|
|
177
|
+
self._skills.clear()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
__all__ = ["SkillLoader"]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Skill data types."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Skill:
|
|
12
|
+
"""Skill definition.
|
|
13
|
+
|
|
14
|
+
A skill represents a reusable capability that can be loaded into an agent.
|
|
15
|
+
Skills are defined by SKILL.md files with YAML frontmatter.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
name: Unique identifier (lowercase + hyphens, ≤64 chars)
|
|
19
|
+
description: Description and trigger conditions (≤1024 chars)
|
|
20
|
+
path: Path to SKILL.md file
|
|
21
|
+
source: Origin of the skill (user/project/system)
|
|
22
|
+
license: Optional license
|
|
23
|
+
allowed_tools: Pre-authorized tools for this skill
|
|
24
|
+
metadata: Additional metadata from frontmatter
|
|
25
|
+
"""
|
|
26
|
+
name: str
|
|
27
|
+
description: str
|
|
28
|
+
path: Path
|
|
29
|
+
source: Literal["user", "project", "system"]
|
|
30
|
+
|
|
31
|
+
# Optional metadata
|
|
32
|
+
license: str | None = None
|
|
33
|
+
allowed_tools: list[str] | None = None
|
|
34
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
# Runtime (lazy loaded)
|
|
37
|
+
_content: str | None = field(default=None, repr=False)
|
|
38
|
+
|
|
39
|
+
async def load_content(self) -> str:
|
|
40
|
+
"""Load full content (Progressive Disclosure).
|
|
41
|
+
|
|
42
|
+
Lazily loads the body content of SKILL.md when needed.
|
|
43
|
+
"""
|
|
44
|
+
if self._content is None:
|
|
45
|
+
self._content = await self._read_body()
|
|
46
|
+
return self._content
|
|
47
|
+
|
|
48
|
+
async def _read_body(self) -> str:
|
|
49
|
+
"""Read SKILL.md body (after frontmatter)."""
|
|
50
|
+
def _read() -> str:
|
|
51
|
+
text = self.path.read_text(encoding="utf-8")
|
|
52
|
+
if text.startswith("---"):
|
|
53
|
+
parts = text.split("---", 2)
|
|
54
|
+
if len(parts) >= 3:
|
|
55
|
+
return parts[2].strip()
|
|
56
|
+
return text
|
|
57
|
+
|
|
58
|
+
loop = asyncio.get_event_loop()
|
|
59
|
+
return await loop.run_in_executor(None, _read)
|
|
60
|
+
|
|
61
|
+
def get_resource_path(self, relative: str) -> Path:
|
|
62
|
+
"""Get absolute path for a resource relative to skill directory.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
relative: Relative path (e.g., "scripts/helper.py")
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Absolute path to the resource
|
|
69
|
+
"""
|
|
70
|
+
return self.path.parent / relative
|
|
71
|
+
|
|
72
|
+
def has_script(self, name: str) -> bool:
|
|
73
|
+
"""Check if skill has a specific script."""
|
|
74
|
+
script_path = self.get_resource_path(f"scripts/{name}")
|
|
75
|
+
return script_path.exists()
|
|
76
|
+
|
|
77
|
+
def has_reference(self, name: str) -> bool:
|
|
78
|
+
"""Check if skill has a specific reference document."""
|
|
79
|
+
ref_path = self.get_resource_path(f"references/{name}")
|
|
80
|
+
return ref_path.exists()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
__all__ = ["Skill"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Tool system with ToolSet, decorators, and built-in tools."""
|
|
2
|
+
from .set import ToolSet
|
|
3
|
+
from .decorator import tool
|
|
4
|
+
from .builtin import (
|
|
5
|
+
PlanTool,
|
|
6
|
+
DelegateTool,
|
|
7
|
+
YieldResultTool,
|
|
8
|
+
AskUserTool,
|
|
9
|
+
ThinkingTool,
|
|
10
|
+
)
|
|
11
|
+
from ..core.types.tool import (
|
|
12
|
+
BaseTool,
|
|
13
|
+
ToolInfo,
|
|
14
|
+
ToolContext,
|
|
15
|
+
ToolResult,
|
|
16
|
+
ToolConfig,
|
|
17
|
+
ToolInvocation,
|
|
18
|
+
ToolInvocationState,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
# Base classes
|
|
23
|
+
"BaseTool",
|
|
24
|
+
"ToolInfo",
|
|
25
|
+
"ToolContext",
|
|
26
|
+
"ToolResult",
|
|
27
|
+
"ToolConfig",
|
|
28
|
+
"ToolInvocation",
|
|
29
|
+
"ToolInvocationState",
|
|
30
|
+
# ToolSet and decorators
|
|
31
|
+
"ToolSet",
|
|
32
|
+
"tool",
|
|
33
|
+
# Built-in tools
|
|
34
|
+
"PlanTool",
|
|
35
|
+
"DelegateTool",
|
|
36
|
+
"YieldResultTool",
|
|
37
|
+
"AskUserTool",
|
|
38
|
+
"ThinkingTool",
|
|
39
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Built-in tools for agents."""
|
|
2
|
+
from .plan import PlanTool
|
|
3
|
+
from .delegate import DelegateTool
|
|
4
|
+
from .yield_result import YieldResultTool
|
|
5
|
+
from .ask_user import AskUserTool
|
|
6
|
+
from .thinking import ThinkingTool
|
|
7
|
+
from .bash import BashTool
|
|
8
|
+
from .read import ReadTool
|
|
9
|
+
from .edit import EditTool
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
# Control flow tools
|
|
13
|
+
"PlanTool",
|
|
14
|
+
"DelegateTool",
|
|
15
|
+
"YieldResultTool",
|
|
16
|
+
# HITL tools
|
|
17
|
+
"AskUserTool",
|
|
18
|
+
"ThinkingTool",
|
|
19
|
+
# File/Shell tools
|
|
20
|
+
"BashTool",
|
|
21
|
+
"ReadTool",
|
|
22
|
+
"EditTool",
|
|
23
|
+
]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Ask user tool - human-in-the-loop interaction.
|
|
2
|
+
|
|
3
|
+
Enables agent to request clarification or confirmation from user.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
from ...core.logging import tool_logger as logger
|
|
10
|
+
from ...core.types.tool import BaseTool, ToolContext, ToolResult
|
|
11
|
+
from ...core.types.block import BlockEvent, BlockKind, BlockOp
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
QuestionType = Literal["text", "confirm", "choice"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AskUserTool(BaseTool):
|
|
18
|
+
"""Request input from the user.
|
|
19
|
+
|
|
20
|
+
Use this tool when you need:
|
|
21
|
+
- Clarification on ambiguous requirements
|
|
22
|
+
- Confirmation before destructive operations
|
|
23
|
+
- User to make a choice between options
|
|
24
|
+
- Additional information not available in context
|
|
25
|
+
|
|
26
|
+
The tool will pause execution and wait for user response.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
_name = "ask_user"
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def name(self) -> str:
|
|
33
|
+
return self._name
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def description(self) -> str:
|
|
37
|
+
return """Ask the user a question and wait for their response.
|
|
38
|
+
|
|
39
|
+
Use this when you need clarification, confirmation, or additional
|
|
40
|
+
information from the user. Supports:
|
|
41
|
+
- text: Open-ended questions
|
|
42
|
+
- confirm: Yes/No questions
|
|
43
|
+
- choice: Multiple choice selection
|
|
44
|
+
|
|
45
|
+
The conversation will pause until the user responds."""
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def parameters(self) -> dict[str, Any]:
|
|
49
|
+
return {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"properties": {
|
|
52
|
+
"question": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "The question to ask the user",
|
|
55
|
+
},
|
|
56
|
+
"type": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"enum": ["text", "confirm", "choice"],
|
|
59
|
+
"description": "Type of response expected",
|
|
60
|
+
"default": "text",
|
|
61
|
+
},
|
|
62
|
+
"choices": {
|
|
63
|
+
"type": "array",
|
|
64
|
+
"items": {"type": "string"},
|
|
65
|
+
"description": "Options for 'choice' type questions",
|
|
66
|
+
},
|
|
67
|
+
"default": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"description": "Default value if user skips",
|
|
70
|
+
},
|
|
71
|
+
"context": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"description": "Additional context to help user answer",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
"required": ["question"],
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async def execute(
|
|
80
|
+
self,
|
|
81
|
+
params: dict[str, Any],
|
|
82
|
+
ctx: ToolContext,
|
|
83
|
+
) -> ToolResult:
|
|
84
|
+
"""Execute ask - request user input."""
|
|
85
|
+
question = params.get("question", "")
|
|
86
|
+
q_type: QuestionType = params.get("type", "text")
|
|
87
|
+
choices = params.get("choices", [])
|
|
88
|
+
default = params.get("default")
|
|
89
|
+
context = params.get("context")
|
|
90
|
+
|
|
91
|
+
logger.info(
|
|
92
|
+
"Asking user",
|
|
93
|
+
extra={"question": question[:50], "type": q_type},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Validate choice type has choices
|
|
97
|
+
if q_type == "choice" and not choices:
|
|
98
|
+
return ToolResult.error("Choice type questions require 'choices' parameter")
|
|
99
|
+
|
|
100
|
+
# Emit ASK block
|
|
101
|
+
await self._emit_ask_block(ctx, question, q_type, choices, default, context)
|
|
102
|
+
|
|
103
|
+
# Build display output
|
|
104
|
+
output_parts = [f"Question: {question}"]
|
|
105
|
+
if context:
|
|
106
|
+
output_parts.append(f"Context: {context}")
|
|
107
|
+
if q_type == "confirm":
|
|
108
|
+
output_parts.append("Type: Yes/No")
|
|
109
|
+
elif q_type == "choice" and choices:
|
|
110
|
+
output_parts.append("Options:")
|
|
111
|
+
for i, choice in enumerate(choices, 1):
|
|
112
|
+
output_parts.append(f" {i}. {choice}")
|
|
113
|
+
if default:
|
|
114
|
+
output_parts.append(f"Default: {default}")
|
|
115
|
+
|
|
116
|
+
# Note: Real implementation would:
|
|
117
|
+
# 1. Emit HITL block/event
|
|
118
|
+
# 2. Set invocation state to "waiting_for_input"
|
|
119
|
+
# 3. Pause agent execution
|
|
120
|
+
# 4. When user responds, resume with answer in context
|
|
121
|
+
|
|
122
|
+
return ToolResult(output="\n".join(output_parts))
|
|
123
|
+
|
|
124
|
+
async def _emit_ask_block(
|
|
125
|
+
self,
|
|
126
|
+
ctx: ToolContext,
|
|
127
|
+
question: str,
|
|
128
|
+
q_type: QuestionType,
|
|
129
|
+
choices: list[str],
|
|
130
|
+
default: str | None,
|
|
131
|
+
context: str | None,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Emit ASK block."""
|
|
134
|
+
emit = getattr(ctx, 'emit', None)
|
|
135
|
+
if emit is None:
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
block = BlockEvent(
|
|
139
|
+
kind=BlockKind.ASK,
|
|
140
|
+
op=BlockOp.APPLY,
|
|
141
|
+
data={
|
|
142
|
+
"question": question,
|
|
143
|
+
"type": q_type,
|
|
144
|
+
"choices": choices,
|
|
145
|
+
"default": default,
|
|
146
|
+
"context": context,
|
|
147
|
+
},
|
|
148
|
+
session_id=ctx.session_id,
|
|
149
|
+
invocation_id=ctx.invocation_id,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
await emit(block)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
__all__ = ["AskUserTool"]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""BashTool - Execute shell commands."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import subprocess
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ...core.types.tool import BaseTool, ToolContext, ToolResult, ToolInfo
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BashTool(BaseTool):
|
|
12
|
+
"""Execute shell commands.
|
|
13
|
+
|
|
14
|
+
Provides the ability to run shell commands with timeout and working directory support.
|
|
15
|
+
|
|
16
|
+
Security Note:
|
|
17
|
+
In production, you should restrict the commands that can be executed,
|
|
18
|
+
or run in a sandboxed environment.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
_name = "bash"
|
|
22
|
+
_description = "Execute shell commands and return the output"
|
|
23
|
+
_parameters = {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"properties": {
|
|
26
|
+
"command": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"description": "The shell command to execute",
|
|
29
|
+
},
|
|
30
|
+
"timeout": {
|
|
31
|
+
"type": "integer",
|
|
32
|
+
"description": "Timeout in seconds (default: 30)",
|
|
33
|
+
"default": 30,
|
|
34
|
+
},
|
|
35
|
+
"working_dir": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "Working directory for the command (optional)",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
"required": ["command"],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def __init__(self, allowed_commands: list[str] | None = None):
|
|
44
|
+
"""Initialize BashTool.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
allowed_commands: List of allowed command prefixes (None = allow all)
|
|
48
|
+
"""
|
|
49
|
+
self._allowed_commands = allowed_commands
|
|
50
|
+
|
|
51
|
+
async def execute(
|
|
52
|
+
self,
|
|
53
|
+
params: dict[str, Any],
|
|
54
|
+
ctx: ToolContext,
|
|
55
|
+
) -> ToolResult:
|
|
56
|
+
"""Execute the shell command."""
|
|
57
|
+
command = params.get("command", "")
|
|
58
|
+
timeout = params.get("timeout", 30)
|
|
59
|
+
working_dir = params.get("working_dir")
|
|
60
|
+
|
|
61
|
+
if not command:
|
|
62
|
+
return ToolResult.error("Command is required")
|
|
63
|
+
|
|
64
|
+
# Security check
|
|
65
|
+
if self._allowed_commands:
|
|
66
|
+
cmd_prefix = command.split()[0] if command.split() else ""
|
|
67
|
+
if cmd_prefix not in self._allowed_commands:
|
|
68
|
+
return ToolResult.error(f"Command '{cmd_prefix}' is not allowed")
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
# Run command
|
|
72
|
+
process = await asyncio.create_subprocess_shell(
|
|
73
|
+
command,
|
|
74
|
+
stdout=asyncio.subprocess.PIPE,
|
|
75
|
+
stderr=asyncio.subprocess.PIPE,
|
|
76
|
+
cwd=working_dir,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
stdout, stderr = await asyncio.wait_for(
|
|
81
|
+
process.communicate(),
|
|
82
|
+
timeout=timeout,
|
|
83
|
+
)
|
|
84
|
+
except asyncio.TimeoutError:
|
|
85
|
+
process.kill()
|
|
86
|
+
return ToolResult.error(f"Command timed out after {timeout} seconds")
|
|
87
|
+
|
|
88
|
+
output = stdout.decode("utf-8", errors="replace")
|
|
89
|
+
error_output = stderr.decode("utf-8", errors="replace")
|
|
90
|
+
|
|
91
|
+
if process.returncode != 0:
|
|
92
|
+
return ToolResult(
|
|
93
|
+
output=f"Exit code: {process.returncode}\n\nSTDOUT:\n{output}\n\nSTDERR:\n{error_output}",
|
|
94
|
+
is_error=True,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
result_output = output
|
|
98
|
+
if error_output:
|
|
99
|
+
result_output += f"\n\nSTDERR:\n{error_output}"
|
|
100
|
+
|
|
101
|
+
return ToolResult(output=result_output.strip() or "(no output)")
|
|
102
|
+
|
|
103
|
+
except Exception as e:
|
|
104
|
+
return ToolResult.error(str(e))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
__all__ = ["BashTool"]
|