agent-yaml 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.
agent_yaml/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """
2
+ agent-yaml: Meta-agent orchestration using declarative YAML workflows.
3
+
4
+ This package provides a Python CLI tool that orchestrates AI CLI agents
5
+ (Claude, Gemini, Qwen, Codex) using declarative YAML specifications.
6
+ """
7
+
8
+ __version__ = "0.1.0"
@@ -0,0 +1,95 @@
1
+ """
2
+ CLI adapter modules for different AI agent tools.
3
+
4
+ Provides tool-specific adapters that translate generic workflow configurations
5
+ into tool-specific CLI invocations for OpenCode, Claude, Gemini, Qwen, and Codex.
6
+ """
7
+
8
+ from .base import BaseAdapter
9
+ from .claude import ClaudeAdapter
10
+ from .codex import CodexAdapter
11
+ from .gemini import GeminiAdapter
12
+ from .opencode import OpenCodeAdapter
13
+ from .qwen import QwenAdapter
14
+
15
+
16
+ class UnsupportedCLIError(Exception):
17
+ """Exception raised when an unsupported or unknown CLI agent is specified."""
18
+
19
+ pass
20
+
21
+
22
+ class CLINotFoundError(Exception):
23
+ """Exception raised when the CLI executable is not found in PATH."""
24
+
25
+ pass
26
+
27
+
28
+ def get_adapter(cli_agent_name: str) -> BaseAdapter:
29
+ """
30
+ Factory function to get the appropriate adapter for a CLI agent.
31
+
32
+ Args:
33
+ cli_agent_name: Name of the CLI agent (opencode, claude, gemini, qwen, codex)
34
+
35
+ Returns:
36
+ BaseAdapter: Instance of the appropriate adapter class
37
+
38
+ Raises:
39
+ UnsupportedCLIError: If the CLI agent name is not recognized
40
+ CLINotFoundError: If the CLI executable is not found in PATH
41
+
42
+ Reference:
43
+ spec/ADAPTER_SPEC.md lines 627-792 for capability detection and translation logic
44
+ """
45
+ # Normalize to lowercase for case-insensitive matching
46
+ cli_name = cli_agent_name.lower().strip()
47
+
48
+ # Map CLI agent names to adapter classes
49
+ adapter_map = {
50
+ "opencode": OpenCodeAdapter,
51
+ "claude": ClaudeAdapter,
52
+ "gemini": GeminiAdapter,
53
+ "qwen": QwenAdapter,
54
+ "codex": CodexAdapter,
55
+ }
56
+
57
+ # Check if CLI agent is supported
58
+ if cli_name not in adapter_map:
59
+ supported_agents = ", ".join(sorted(adapter_map.keys()))
60
+ raise UnsupportedCLIError(
61
+ f"Unsupported CLI agent: '{cli_agent_name}'. Supported agents are: {supported_agents}"
62
+ )
63
+
64
+ # Create adapter instance
65
+ adapter_class = adapter_map[cli_name]
66
+ adapter = adapter_class()
67
+
68
+ # Validate that CLI executable is available
69
+ if not adapter.check_available():
70
+ # Get the expected executable name based on the adapter
71
+ if cli_name == "claude":
72
+ executable_hint = "claude-code or claude"
73
+ else:
74
+ executable_hint = cli_name
75
+
76
+ raise CLINotFoundError(
77
+ f"CLI executable not found for '{cli_agent_name}'. "
78
+ f"Please ensure '{executable_hint}' is installed and available in PATH. "
79
+ f"Run 'which {executable_hint.split()[0]}' to verify installation."
80
+ )
81
+
82
+ return adapter
83
+
84
+
85
+ __all__ = [
86
+ "BaseAdapter",
87
+ "ClaudeAdapter",
88
+ "GeminiAdapter",
89
+ "QwenAdapter",
90
+ "CodexAdapter",
91
+ "OpenCodeAdapter",
92
+ "UnsupportedCLIError",
93
+ "CLINotFoundError",
94
+ "get_adapter",
95
+ ]
@@ -0,0 +1,55 @@
1
+ """
2
+ Base adapter abstract class.
3
+
4
+ Defines the interface that all CLI adapters must implement.
5
+ Provides common functionality for command building and subprocess execution.
6
+
7
+ All adapters translate generic configurations to tool-specific CLI arguments:
8
+ - skip-confirmations → tool-specific flag
9
+ - additional-dirs → tool-specific workspace/directory flag
10
+ - timeout → subprocess timeout (not CLI arg)
11
+ - mcp-config-file → tool-specific MCP configuration
12
+
13
+ Reference: spec/ADAPTER_SPEC.md for complete mapping specifications.
14
+ """
15
+
16
+ from abc import ABC, abstractmethod
17
+
18
+
19
+ class BaseAdapter(ABC):
20
+ """Abstract base class for CLI adapters."""
21
+
22
+ def __init__(self, tool_name, tool_config=None):
23
+ """
24
+ Initialize adapter.
25
+
26
+ Args:
27
+ tool_name: Name of the CLI tool (claude, gemini, qwen, codex)
28
+ tool_config: Optional tool-specific configuration
29
+ """
30
+ self.tool_name = tool_name
31
+ self.tool_config = tool_config or {}
32
+
33
+ @abstractmethod
34
+ def build_command(self, task_config, prompt):
35
+ """
36
+ Build CLI command from task configuration.
37
+
38
+ Args:
39
+ task_config: Task configuration dict
40
+ prompt: Interpolated prompt string
41
+
42
+ Returns:
43
+ list: Command as list of strings (argv format)
44
+ """
45
+ pass
46
+
47
+ @abstractmethod
48
+ def check_available(self):
49
+ """
50
+ Check if CLI tool is available in PATH.
51
+
52
+ Returns:
53
+ bool: True if tool is available
54
+ """
55
+ pass
@@ -0,0 +1,144 @@
1
+ """
2
+ Claude CLI adapter.
3
+
4
+ Maps generic configurations to Claude-specific CLI arguments:
5
+ - skip-confirmations: true → --dangerously-skip-permissions
6
+ - additional-dirs: ["/tmp"] → --add-dir="/tmp"
7
+ - timeout: 3600 → subprocess timeout (not CLI arg)
8
+ - mcp-config-file: "..." → --mcp-config "..."
9
+ - tools: "default" (default) → --tools "default" (enable all tools in print mode)
10
+ - output-format: "stream-json" → --output-format "stream-json" (streaming output)
11
+
12
+ CLI tool: claude-code or claude (detection via which)
13
+
14
+ Default behavior: Uses --print mode with tools enabled for non-interactive agentic execution.
15
+
16
+ Reference: spec/ADAPTER_SPEC.md for complete Claude mappings.
17
+ """
18
+
19
+ import shutil
20
+
21
+ from .base import BaseAdapter
22
+
23
+
24
+ class ClaudeAdapter(BaseAdapter):
25
+ """Adapter for Claude CLI (claude-code)."""
26
+
27
+ # Model name translation mapping
28
+ MODEL_MAPPING = {
29
+ "sonnet-4.5": "claude-sonnet-4-5-20250929",
30
+ "opus-4": "claude-opus-4",
31
+ }
32
+
33
+ def __init__(self, tool_config=None):
34
+ """Initialize Claude adapter."""
35
+ super().__init__("claude", tool_config)
36
+
37
+ def build_command(self, task_config, prompt):
38
+ """
39
+ Build Claude CLI command from task configuration.
40
+
41
+ Args:
42
+ task_config: Task configuration dict
43
+ prompt: Interpolated prompt string
44
+
45
+ Returns:
46
+ list: Command as list of strings (argv format)
47
+ """
48
+ # Start with base command - check both "claude" and "claude-code"
49
+ cmd = []
50
+ if shutil.which("claude-code"):
51
+ cmd.append("claude-code")
52
+ elif shutil.which("claude"):
53
+ cmd.append("claude")
54
+ else:
55
+ cmd.append("claude") # Default, will fail in check_available
56
+
57
+ # Process config options in order per spec
58
+
59
+ # 1. skip-confirmations: true → --dangerously-skip-permissions
60
+ if task_config.get("skip-confirmations"):
61
+ cmd.append("--dangerously-skip-permissions")
62
+
63
+ # 2. additional-dirs: ["/tmp"] → --add-dir="/tmp" (repeat for each)
64
+ additional_dirs = task_config.get("additional-dirs", [])
65
+ for directory in additional_dirs:
66
+ cmd.append(f"--add-dir={directory}")
67
+
68
+ # 3. model: "sonnet-4.5" → --model "claude-sonnet-4-5-20250929" (with translation)
69
+ model = task_config.get("model")
70
+ if model:
71
+ # Translate generic model name to Claude-specific identifier
72
+ translated_model = self.MODEL_MAPPING.get(model, model)
73
+ cmd.extend(["--model", translated_model])
74
+
75
+ # 4. debug: true → --debug
76
+ if task_config.get("debug"):
77
+ cmd.append("--debug")
78
+
79
+ # 5. tools: "default" → --tools "default" (enable tools in print mode)
80
+ # Default to "default" which enables all tools for agentic execution
81
+ tools = task_config.get("tools", "default")
82
+ if tools:
83
+ cmd.extend(["--tools", tools])
84
+
85
+ # 6. output-format: "stream-json" → --output-format "stream-json"
86
+ # Default to "text" for simplicity, use "stream-json" in debug mode for streaming
87
+ output_format = task_config.get("output-format")
88
+ if output_format is None:
89
+ # Auto-select: stream-json in debug mode, text otherwise
90
+ output_format = "stream-json" if task_config.get("debug") else "text"
91
+
92
+ # stream-json requires --verbose flag
93
+ if output_format == "stream-json":
94
+ cmd.append("--verbose")
95
+ cmd.extend(["--output-format", "stream-json"])
96
+ elif output_format != "text":
97
+ cmd.extend(["--output-format", output_format])
98
+
99
+ # 7. session-id: "id" → --session-id "id"
100
+ session_id = task_config.get("session-id")
101
+ if session_id:
102
+ cmd.extend(["--session-id", session_id])
103
+
104
+ # 8. mcp-servers: [notion] → --allowed-mcp-server-names notion
105
+ mcp_servers = task_config.get("mcp-servers", [])
106
+ if mcp_servers:
107
+ cmd.append("--allowed-mcp-server-names")
108
+ cmd.extend(mcp_servers)
109
+
110
+ # 9. mcp-config-file: "path" → --mcp-config "path"
111
+ mcp_config_file = task_config.get("mcp-config-file")
112
+ if mcp_config_file:
113
+ cmd.extend(["--mcp-config", mcp_config_file])
114
+
115
+ # 10. permissions: ["plan"] → --permission-mode plan
116
+ permissions = task_config.get("permissions", [])
117
+ if permissions:
118
+ # Take first permission mode
119
+ cmd.extend(["--permission-mode", permissions[0]])
120
+
121
+ # 11. raw-args: [...] → append directly at end
122
+ raw_args = task_config.get("raw-args", [])
123
+ if raw_args:
124
+ cmd.extend(raw_args)
125
+
126
+ # 12. timeout: 3600 → store but DON'T add to CLI args (used for subprocess timeout)
127
+ # Note: timeout is handled by executor, not part of CLI command
128
+
129
+ # 13. Add prompt in print mode (non-interactive agentic execution)
130
+ # Always use --print mode with tools enabled for reliable non-interactive execution
131
+ cmd.extend(["--print", prompt])
132
+
133
+ return cmd
134
+
135
+ def check_available(self):
136
+ """
137
+ Check if Claude CLI is available in PATH.
138
+
139
+ Checks for both "claude" and "claude-code" executables.
140
+
141
+ Returns:
142
+ bool: True if tool is available
143
+ """
144
+ return shutil.which("claude-code") is not None or shutil.which("claude") is not None
@@ -0,0 +1,112 @@
1
+ """
2
+ Codex CLI adapter.
3
+
4
+ Maps generic configurations to Codex-specific CLI arguments:
5
+ - skip-confirmations: true → --full-auto
6
+ - additional-dirs: ["/tmp"] → -C /tmp (ONLY first directory, warn if multiple)
7
+ - model: pass through as-is (no translation)
8
+ - debug: true → -c debug=true (config style, NOT --debug)
9
+ - permissions: ["on-failure"] → -a on-failure
10
+ - web-search: true → --search
11
+ - PROMPT: DIRECT argument at end (NO flag)
12
+
13
+ Unsupported features (log warning):
14
+ - output-format
15
+
16
+ CLI tool: codex exec (uses exec subcommand for non-interactive execution)
17
+
18
+ Reference: spec/ADAPTER_SPEC.md for complete Codex mappings.
19
+ """
20
+
21
+ import logging
22
+ import shutil
23
+
24
+ from .base import BaseAdapter
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class CodexAdapter(BaseAdapter):
30
+ """Adapter for Codex CLI."""
31
+
32
+ def __init__(self, tool_config=None):
33
+ """Initialize Codex adapter."""
34
+ super().__init__("codex", tool_config)
35
+
36
+ def build_command(self, task_config, prompt):
37
+ """
38
+ Build Codex CLI command from task configuration.
39
+
40
+ Args:
41
+ task_config: Task configuration dict
42
+ prompt: Interpolated prompt string
43
+
44
+ Returns:
45
+ list: Command as list of strings (argv format)
46
+ """
47
+ # Start with base command - use 'exec' subcommand for non-interactive execution
48
+ cmd = ["codex", "exec"]
49
+
50
+ # Always add --skip-git-repo-check to allow running in any directory
51
+ cmd.append("--skip-git-repo-check")
52
+
53
+ # Process config options in order per spec
54
+
55
+ # 1. skip-confirmations: true → --full-auto
56
+ if task_config.get("skip-confirmations"):
57
+ cmd.append("--full-auto")
58
+
59
+ # 2. additional-dirs: ["/tmp"] → -C /tmp (ONLY first directory)
60
+ additional_dirs = task_config.get("additional-dirs", [])
61
+ if additional_dirs:
62
+ # Codex only supports one directory
63
+ cmd.extend(["-C", additional_dirs[0]])
64
+ if len(additional_dirs) > 1:
65
+ logger.warning(
66
+ f"Codex only supports one directory. Using first directory '{additional_dirs[0]}'. "
67
+ f"Additional directories will be ignored: {additional_dirs[1:]}"
68
+ )
69
+
70
+ # 3. model: pass through as-is (no translation)
71
+ model = task_config.get("model")
72
+ if model:
73
+ cmd.extend(["--model", model])
74
+
75
+ # 4. debug: true → -c debug=true (config style)
76
+ if task_config.get("debug"):
77
+ cmd.extend(["-c", "debug=true"])
78
+
79
+ # 5. output-format: NOT SUPPORTED
80
+ if task_config.get("output-format"):
81
+ logger.warning("Codex does not support output-format configuration")
82
+
83
+ # 6. permissions: ["on-failure"] → -a on-failure
84
+ permissions = task_config.get("permissions", [])
85
+ if permissions:
86
+ # Take first permission mode
87
+ cmd.extend(["-a", permissions[0]])
88
+
89
+ # 7. web-search: true → --search
90
+ if task_config.get("web-search"):
91
+ cmd.append("--search")
92
+
93
+ # 8. raw-args: passthrough (no translation)
94
+ raw_args = task_config.get("raw-args", [])
95
+ if raw_args:
96
+ cmd.extend(raw_args)
97
+
98
+ # 9. Prompt goes at the end as DIRECT argument (NO flag)
99
+ cmd.append(prompt)
100
+
101
+ return cmd
102
+
103
+ def check_available(self):
104
+ """
105
+ Check if Codex CLI is available in PATH.
106
+
107
+ Checks for "codex" executable.
108
+
109
+ Returns:
110
+ bool: True if tool is available
111
+ """
112
+ return shutil.which("codex") is not None
@@ -0,0 +1,141 @@
1
+ """
2
+ Gemini CLI adapter.
3
+
4
+ Maps generic configurations to Gemini-specific CLI arguments:
5
+ - skip-confirmations: true → --yolo or --approval-mode yolo
6
+ - additional-dirs: ["/tmp"] → --include-directories /tmp (space-separated)
7
+ - model: pass through as-is (no translation)
8
+ - permissions: ["plan"] → --approval-mode plan
9
+ - mcp-servers: [notion] → --allowed-mcp-server-names notion
10
+ - extensions: [...] → --extensions ...
11
+ - checkpointing: true → --checkpointing
12
+ - screen-reader: true → --screen-reader
13
+ - telemetry: true → --telemetry
14
+ - web-search: true → BUILT-IN (no flag needed)
15
+ - output-format: "json" → --output-format json
16
+
17
+ Unsupported features (log warning):
18
+ - session-id
19
+ - mcp-config-file
20
+
21
+ CLI tool: gemini
22
+
23
+ Reference: spec/ADAPTER_SPEC.md for complete Gemini mappings.
24
+ """
25
+
26
+ import logging
27
+ import shutil
28
+
29
+ from .base import BaseAdapter
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class GeminiAdapter(BaseAdapter):
35
+ """Adapter for Gemini CLI."""
36
+
37
+ def __init__(self, tool_config=None):
38
+ """Initialize Gemini adapter."""
39
+ super().__init__("gemini", tool_config)
40
+
41
+ def build_command(self, task_config, prompt):
42
+ """
43
+ Build Gemini CLI command from task configuration.
44
+
45
+ Args:
46
+ task_config: Task configuration dict
47
+ prompt: Interpolated prompt string
48
+
49
+ Returns:
50
+ list: Command as list of strings (argv format)
51
+ """
52
+ # Start with base command
53
+ cmd = ["gemini"]
54
+
55
+ # Process config options in order per spec
56
+
57
+ # 1. skip-confirmations: true → --yolo
58
+ if task_config.get("skip-confirmations"):
59
+ cmd.append("--yolo")
60
+
61
+ # 2. additional-dirs: ["/tmp"] → --include-directories /tmp (space-separated)
62
+ additional_dirs = task_config.get("additional-dirs", [])
63
+ if additional_dirs:
64
+ cmd.append("--include-directories")
65
+ cmd.extend(additional_dirs)
66
+
67
+ # 3. model: pass through as-is (no translation)
68
+ model = task_config.get("model")
69
+ if model:
70
+ cmd.extend(["--model", model])
71
+
72
+ # 4. debug: true → --debug
73
+ if task_config.get("debug"):
74
+ cmd.append("--debug")
75
+
76
+ # 5. output-format: "json" → --output-format json
77
+ output_format = task_config.get("output-format")
78
+ if output_format:
79
+ cmd.extend(["--output-format", output_format])
80
+
81
+ # 6. session-id: NOT SUPPORTED
82
+ if task_config.get("session-id"):
83
+ logger.warning("Gemini does not support session-id configuration")
84
+
85
+ # 7. mcp-servers: [notion] → --allowed-mcp-server-names notion
86
+ mcp_servers = task_config.get("mcp-servers", [])
87
+ if mcp_servers:
88
+ cmd.append("--allowed-mcp-server-names")
89
+ cmd.extend(mcp_servers)
90
+
91
+ # 8. mcp-config-file: NOT SUPPORTED
92
+ if task_config.get("mcp-config-file"):
93
+ logger.warning("Gemini does not support mcp-config-file configuration")
94
+
95
+ # 9. permissions: ["plan"] → --approval-mode plan
96
+ permissions = task_config.get("permissions", [])
97
+ if permissions:
98
+ # Take first permission mode
99
+ cmd.extend(["--approval-mode", permissions[0]])
100
+
101
+ # 10. extensions: [...] → --extensions ...
102
+ extensions = task_config.get("extensions", [])
103
+ if extensions:
104
+ cmd.append("--extensions")
105
+ cmd.extend(extensions)
106
+
107
+ # 11. checkpointing: true → --checkpointing
108
+ if task_config.get("checkpointing"):
109
+ cmd.append("--checkpointing")
110
+
111
+ # 12. telemetry: true → --telemetry
112
+ if task_config.get("telemetry"):
113
+ cmd.append("--telemetry")
114
+
115
+ # 13. screen-reader: true → --screen-reader
116
+ if task_config.get("screen-reader"):
117
+ cmd.append("--screen-reader")
118
+
119
+ # 14. web-search: BUILT-IN (no flag needed)
120
+ # web-search is built-in for Gemini, don't add any flag
121
+
122
+ # 15. raw-args: passthrough (no translation)
123
+ raw_args = task_config.get("raw-args", [])
124
+ if raw_args:
125
+ cmd.extend(raw_args)
126
+
127
+ # 16. Prompt goes at the end with -p flag
128
+ cmd.extend(["-p", prompt])
129
+
130
+ return cmd
131
+
132
+ def check_available(self):
133
+ """
134
+ Check if Gemini CLI is available in PATH.
135
+
136
+ Checks for "gemini" executable.
137
+
138
+ Returns:
139
+ bool: True if tool is available
140
+ """
141
+ return shutil.which("gemini") is not None