File size: 4,330 Bytes
82cc20c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
"""Local shell executor: run shell_call commands and return shell_call_output."""

import re
import subprocess
import yaml
from pathlib import Path
from core.config_loader import BASE_DIR, PATHS, IS_WINDOWS

SHELL_TIMEOUT = 30

# Match `type/cat foo.md` (single-line), used to short-circuit pure file reads
# back to Python — saves a cmd.exe spawn (~50-200ms on Windows).
_FILE_READ_RE = re.compile(
    r"^(?:type|cat)\s+[\"']?([^\"|']+\.md)[\"']?\s*$",
    re.IGNORECASE,
)

# {path_fragment: skill_name} — both absolute and relative forms are listed
# because the model may emit either (relative is typical: `cat skills/foo/...`).
SKILL_PATHS = {
    p: name
    for name, rel_path in PATHS.get("skills", {}).items()
    for p in (
        rel_path.replace("\\", "/"),
        str(BASE_DIR / rel_path).replace("\\", "/"),
    )
}


def parse_skill_description(skill_md_path):
    """Parse description from SKILL.md YAML frontmatter."""
    content = skill_md_path.read_text(encoding="utf-8")
    parts = content.split("---", 2)
    if len(parts) >= 3:
        frontmatter = yaml.safe_load(parts[1])
        if isinstance(frontmatter, dict) and frontmatter.get("description"):
            return frontmatter["description"]
    return None


def build_tools():
    """Build the tools list from config skill paths."""
    skills = []
    for name, rel_path in PATHS["skills"].items():
        skill_dir = BASE_DIR / rel_path
        skill_md = skill_dir / "SKILL.md"
        description = f"Skill: {name}"
        if skill_md.exists():
            description = parse_skill_description(skill_md) or description
        skills.append({
            "name": name,
            "description": description,
            "path": str(skill_dir),
        })
    return [{
        "type": "shell",
        "environment": {"type": "local", "skills": skills},
    }]



def detect_skills(commands):
    """Detect which skill(s) a set of commands reference, based on path fragments."""
    triggered = set()
    for cmd in commands:
        normalized_cmd = cmd.replace("\\", "/")
        for path_fragment, skill_name in SKILL_PATHS.items():
            if path_fragment in normalized_cmd:
                triggered.add(skill_name)
    return sorted(triggered)


def execute_shell_call(shell_call):
    """Execute a shell_call locally. Returns (shell_call_output_dict, list_of_skill_names)."""
    commands = getattr(shell_call.action, "commands", [])
    results = []

    for cmd in commands:
        # Short-circuit `type/cat *.md` → read the file directly, skipping the
        # cmd.exe / sh spawn (saves ~50-200ms per read on Windows).
        m = _FILE_READ_RE.match(cmd.strip())
        if m:
            rel = m.group(1).replace("\\", "/")
            path = (BASE_DIR / rel).resolve()
            try:
                results.append({
                    "stdout": path.read_text(encoding="utf-8", errors="replace"),
                    "stderr": "",
                    "outcome": {"type": "exit", "exit_code": 0},
                })
            except OSError as e:
                results.append({
                    "stdout": "",
                    "stderr": f"{type(e).__name__}: {e}",
                    "outcome": {"type": "exit", "exit_code": 1},
                })
            continue

        try:
            proc = subprocess.run(
                cmd,
                shell=True,
                capture_output=True,
                text=True,
                encoding="utf-8",
                errors="replace",
                timeout=SHELL_TIMEOUT,
                cwd=str(BASE_DIR),
            )
            results.append({
                # OpenAI rejects null stdout/stderr — coerce to empty string.
                "stdout": proc.stdout or "",
                "stderr": proc.stderr or "",
                "outcome": {"type": "exit", "exit_code": proc.returncode},
            })
        except subprocess.TimeoutExpired:
            results.append({
                "stdout": "",
                "stderr": f"Command timed out after {SHELL_TIMEOUT} seconds.",
                "outcome": {"type": "timeout"},
            })

    skills_used = detect_skills(commands)

    return {
        "type": "shell_call_output",
        "call_id": shell_call.call_id,
        "output": results,
    }, skills_used