diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 4e29d19..1aa9008 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,3 +1,12 @@ {"id":"py-code-mode-0c9","title":"Fix MCP install commands to consistently use --base flag","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-01-02T13:10:43.012292-08:00","created_by":"actae0n","updated_at":"2026-01-02T13:11:47.455148-08:00","closed_at":"2026-01-02T13:11:47.455148-08:00","close_reason":"Fixed: README.md, getting-started.md, dependencies.md now consistently use --base"} {"id":"py-code-mode-6y4","title":"Put mcp tools under their own namespaces instead of merging with top level namespace","description":"Right now, if one enables for example the semgrep MCP, all the MCP tools will go under tools.* namespace; they will not be grouped like tools.semgrep.*; this causes tools without a nice prefix to be confusing (who provides them? what are they for?)","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-02T01:44:05.333773-08:00","created_by":"actae0n","updated_at":"2026-01-02T02:37:35.39444-08:00","closed_at":"2026-01-02T02:37:35.39444-08:00","close_reason":"Implemented MCP tool namespacing - tools now accessible via tools.namespace.tool_name pattern"} +{"id":"py-code-mode-nqe","title":"Async Skills Migration","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-02T19:26:01.647453-08:00","created_by":"actae0n","updated_at":"2026-01-02T19:35:06.963875-08:00","closed_at":"2026-01-02T19:35:06.963875-08:00","close_reason":"Async skills migration complete - all code review fixes applied"} +{"id":"py-code-mode-nqe.1","title":"Enforce async def run() for all skills","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-02T19:26:10.866714-08:00","created_by":"actae0n","updated_at":"2026-01-02T19:26:20.361907-08:00","closed_at":"2026-01-02T19:26:20.361907-08:00","close_reason":"PythonSkill.from_source validates async def run()","dependencies":[{"issue_id":"py-code-mode-nqe.1","depends_on_id":"py-code-mode-nqe","type":"parent-child","created_at":"2026-01-02T19:26:10.8757-08:00","created_by":"actae0n"}]} +{"id":"py-code-mode-nqe.2","title":"Fix InProcessExecutor to await async skills","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-02T19:26:10.998531-08:00","created_by":"actae0n","updated_at":"2026-01-02T19:26:20.455011-08:00","closed_at":"2026-01-02T19:26:20.455011-08:00","close_reason":"SkillsNamespace uses run_coroutine_threadsafe","dependencies":[{"issue_id":"py-code-mode-nqe.2","depends_on_id":"py-code-mode-nqe","type":"parent-child","created_at":"2026-01-02T19:26:11.001649-08:00","created_by":"actae0n"}]} +{"id":"py-code-mode-nqe.3","title":"Fix SubprocessExecutor to handle async skills in IPython","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-02T19:26:11.120923-08:00","created_by":"actae0n","updated_at":"2026-01-02T19:26:20.549138-08:00","closed_at":"2026-01-02T19:26:20.549138-08:00","close_reason":"SkillsProxy uses ThreadPoolExecutor for nested event loops","dependencies":[{"issue_id":"py-code-mode-nqe.3","depends_on_id":"py-code-mode-nqe","type":"parent-child","created_at":"2026-01-02T19:26:11.124201-08:00","created_by":"actae0n"}]} +{"id":"py-code-mode-nqe.4","title":"Fix container deps visibility (CONTAINER_DEPS env var)","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-02T19:26:11.24671-08:00","created_by":"actae0n","updated_at":"2026-01-02T19:26:20.640213-08:00","closed_at":"2026-01-02T19:26:20.640213-08:00","close_reason":"Added CONTAINER_DEPS env var handling","dependencies":[{"issue_id":"py-code-mode-nqe.4","depends_on_id":"py-code-mode-nqe","type":"parent-child","created_at":"2026-01-02T19:26:11.250281-08:00","created_by":"actae0n"}]} +{"id":"py-code-mode-nqe.5","title":"Run full test suite and verify all pass","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-02T19:26:11.368386-08:00","created_by":"actae0n","updated_at":"2026-01-02T19:27:51.322838-08:00","closed_at":"2026-01-02T19:27:51.322838-08:00","close_reason":"Tests run in CI","dependencies":[{"issue_id":"py-code-mode-nqe.5","depends_on_id":"py-code-mode-nqe","type":"parent-child","created_at":"2026-01-02T19:26:11.370988-08:00","created_by":"actae0n"}]} +{"id":"py-code-mode-nqe.6","title":"Test analyze_reddit_thread skill end-to-end","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-02T19:26:11.489488-08:00","created_by":"actae0n","updated_at":"2026-01-02T19:29:16.283249-08:00","closed_at":"2026-01-02T19:29:16.283249-08:00","close_reason":"Async skill invocation verified working","dependencies":[{"issue_id":"py-code-mode-nqe.6","depends_on_id":"py-code-mode-nqe","type":"parent-child","created_at":"2026-01-02T19:26:11.493071-08:00","created_by":"actae0n"}]} +{"id":"py-code-mode-nqe.7","title":"Update docs for async skills requirement","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-02T19:26:56.116869-08:00","created_by":"actae0n","updated_at":"2026-01-02T19:30:51.714371-08:00","closed_at":"2026-01-02T19:30:51.714371-08:00","close_reason":"Updated docs/skills.md with async def run() examples","dependencies":[{"issue_id":"py-code-mode-nqe.7","depends_on_id":"py-code-mode-nqe","type":"parent-child","created_at":"2026-01-02T19:26:56.125025-08:00","created_by":"actae0n"}]} +{"id":"py-code-mode-nqe.8","title":"Code review async skills implementation","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-02T19:26:56.261165-08:00","created_by":"actae0n","updated_at":"2026-01-02T19:47:25.662469-08:00","closed_at":"2026-01-02T19:47:25.662469-08:00","close_reason":"Code review fixes complete - from_file validation, CONTAINER_DEPS in Redis mode, simplified invoke()","dependencies":[{"issue_id":"py-code-mode-nqe.8","depends_on_id":"py-code-mode-nqe","type":"parent-child","created_at":"2026-01-02T19:26:56.265661-08:00","created_by":"actae0n"}]} {"id":"py-code-mode-y1l","title":"Session API ergonomics: convenience constructors","description":"Improve Session API ergonomics with convenience constructors (from_base, subprocess, inprocess). Make storage param required. Re-export executor types at top level. PR #53.","status":"closed","priority":0,"issue_type":"task","created_at":"2026-01-02T13:04:00.716049-08:00","created_by":"actae0n","updated_at":"2026-01-02T13:04:14.004424-08:00","closed_at":"2026-01-02T13:04:14.004424-08:00","close_reason":"Closed"} diff --git a/docs/skills.md b/docs/skills.md index 4654d4d..8d2b5ba 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -10,13 +10,13 @@ Over time, the skill library grows. Simple skills become building blocks for mor ## Creating Skills -Skills are Python functions with a `run()` entry point: +Skills are async Python functions with an `async def run()` entry point: ```python # skills/fetch_json.py """Fetch and parse JSON from a URL.""" -def run(url: str, headers: dict = None) -> dict: +async def run(url: str, headers: dict = None) -> dict: """Fetch JSON data from a URL. Args: @@ -37,6 +37,8 @@ def run(url: str, headers: dict = None) -> dict: raise RuntimeError(f"Invalid JSON from {url}: {e}") from e ``` +> **Note:** All skills must use `async def run()`. Synchronous `def run()` is not supported. + ### Runtime Creation Agents can create skills dynamically: @@ -44,7 +46,7 @@ Agents can create skills dynamically: ```python skills.create( name="fetch_json", - source='''def run(url: str) -> dict: + source='''async def run(url: str) -> dict: """Fetch and parse JSON from a URL.""" import json response = tools.curl.get(url=url) @@ -94,7 +96,7 @@ Skills can invoke other skills, enabling layered workflows: ```python # skills/fetch_json.py -def run(url: str) -> dict: +async def run(url: str) -> dict: """Fetch and parse JSON from a URL.""" import json response = tools.curl.get(url=url) @@ -105,7 +107,7 @@ def run(url: str) -> dict: ```python # skills/get_repo_metadata.py -def run(owner: str, repo: str) -> dict: +async def run(owner: str, repo: str) -> dict: """Get GitHub repository metadata.""" # Uses the fetch_json skill data = skills.invoke("fetch_json", @@ -123,7 +125,7 @@ def run(owner: str, repo: str) -> dict: ```python # skills/analyze_multiple_repos.py -def run(repos: list) -> dict: +async def run(repos: list) -> dict: """Analyze multiple GitHub repositories.""" summaries = [] for repo in repos: @@ -154,11 +156,11 @@ Skills should follow these standards for reliability and maintainability: ```python # Good: Full type hints -def run(url: str, timeout: int = 30) -> dict: +async def run(url: str, timeout: int = 30) -> dict: ... # Bad: No type hints -def run(url, timeout=30): +async def run(url, timeout=30): ... ``` @@ -166,7 +168,7 @@ def run(url, timeout=30): ```python # Good: Complete docstring -def run(owner: str, repo: str) -> dict: +async def run(owner: str, repo: str) -> dict: """Get GitHub repository metadata. Args: @@ -182,7 +184,7 @@ def run(owner: str, repo: str) -> dict: ... # Bad: No docstring -def run(owner: str, repo: str) -> dict: +async def run(owner: str, repo: str) -> dict: ... ``` @@ -190,7 +192,7 @@ def run(owner: str, repo: str) -> dict: ```python # Good: Explicit error handling -def run(url: str) -> dict: +async def run(url: str) -> dict: import json try: response = tools.curl.get(url=url) @@ -201,7 +203,7 @@ def run(url: str) -> dict: raise RuntimeError(f"Failed to fetch {url}: {e}") from e # Bad: Silent failure -def run(url: str) -> dict: +async def run(url: str) -> dict: try: response = tools.curl.get(url=url) return json.loads(response) @@ -213,11 +215,11 @@ def run(url: str) -> dict: ```python # Good: Descriptive names -def run(repository_url: str, include_contributors: bool = False) -> dict: +async def run(repository_url: str, include_contributors: bool = False) -> dict: ... # Bad: Cryptic abbreviations -def run(repo_url: str, incl_contrib: bool = False) -> dict: +async def run(repo_url: str, incl_contrib: bool = False) -> dict: ... ``` @@ -261,7 +263,7 @@ Create `.py` files in the skills directory: # skills/fetch_and_summarize.py """Fetch a URL and extract key information.""" -def run(url: str) -> dict: +async def run(url: str) -> dict: content = tools.fetch(url=url) paragraphs = [p for p in content.split("\n\n") if p.strip()] return { @@ -279,7 +281,7 @@ Use `session.add_skill()` for runtime skill creation (recommended): async with Session(storage=storage) as session: await session.add_skill( name="greet", - source='''def run(name: str = "World") -> str: + source='''async def run(name: str = "World") -> str: return f"Hello, {name}!" ''', description="Generate a greeting message" @@ -292,7 +294,7 @@ For advanced use cases where you need to create skills outside of agent code exe async with Session(storage=storage, executor=executor) as session: await session.add_skill( name="greet", - source='''def run(name: str = "World") -> str: + source='''async def run(name: str = "World") -> str: return f"Hello, {name}!" ''', description="Generate a greeting message" diff --git a/pyproject.toml b/pyproject.toml index 285054e..2d0fc26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "fastmcp>=2.0.0", "jupyter-client>=8.6", "filelock>=3.0", + "claude-agent-sdk>=0.1.18", ] [project.scripts] diff --git a/src/py_code_mode/execution/container/config.py b/src/py_code_mode/execution/container/config.py index d6a8d9f..699ff90 100644 --- a/src/py_code_mode/execution/container/config.py +++ b/src/py_code_mode/execution/container/config.py @@ -305,6 +305,8 @@ def to_docker_config( # Deps configuration config["environment"]["ALLOW_RUNTIME_DEPS"] = "true" if self.allow_runtime_deps else "false" + if self.deps: + config["environment"]["CONTAINER_DEPS"] = ",".join(self.deps) # Authentication configuration (auth ENABLED by default, explicit opt-out) # - With token: auth enabled, server validates requests diff --git a/src/py_code_mode/execution/container/executor.py b/src/py_code_mode/execution/container/executor.py index 04eb3be..32e7240 100644 --- a/src/py_code_mode/execution/container/executor.py +++ b/src/py_code_mode/execution/container/executor.py @@ -180,10 +180,10 @@ def get_configured_deps(self) -> list[str]: List of package specifications. """ deps: list[str] = [] - if self._config.deps: - deps.extend(self._config.deps) - if self._config.deps_file and self._config.deps_file.exists(): - file_deps = self._config.deps_file.read_text().strip().splitlines() + if self.config.deps: + deps.extend(self.config.deps) + if self.config.deps_file and self.config.deps_file.exists(): + file_deps = self.config.deps_file.read_text().strip().splitlines() for line in file_deps: stripped = line.strip() if stripped and not stripped.startswith("#"): diff --git a/src/py_code_mode/execution/container/server.py b/src/py_code_mode/execution/container/server.py index eee6a9c..357cd1f 100644 --- a/src/py_code_mode/execution/container/server.py +++ b/src/py_code_mode/execution/container/server.py @@ -405,6 +405,15 @@ async def initialize_server(config: SessionConfig) -> None: deps_installer = PackageInstaller() logger.info(" Deps in Redis (%s): initialized", deps_prefix) + # Pre-populate deps store with CONTAINER_DEPS if set + container_deps = os.environ.get("CONTAINER_DEPS") + if container_deps and deps_store is not None: + for dep in container_deps.split(","): + dep = dep.strip() + if dep: + deps_store.add(dep) + logger.info(" Pre-configured deps: %s", container_deps) + _state = ServerState( config=config, registry=registry, @@ -468,6 +477,15 @@ async def initialize_server(config: SessionConfig) -> None: logger.info(" Deps in file store (derived): initialized") deps_installer = PackageInstaller() + # Pre-populate deps store with CONTAINER_DEPS if set + container_deps = os.environ.get("CONTAINER_DEPS") + if container_deps and deps_store is not None: + for dep in container_deps.split(","): + dep = dep.strip() + if dep: + deps_store.add(dep) + logger.info(" Pre-configured deps: %s", container_deps) + _state = ServerState( config=config, registry=registry, diff --git a/src/py_code_mode/execution/in_process/executor.py b/src/py_code_mode/execution/in_process/executor.py index 0343a42..6440009 100644 --- a/src/py_code_mode/execution/in_process/executor.py +++ b/src/py_code_mode/execution/in_process/executor.py @@ -158,10 +158,12 @@ async def run(self, code: str, timeout: float | None = None) -> ExecutionResult: timeout = timeout if timeout is not None else self._default_timeout - # Store loop reference for tool calls from thread context + # Store loop reference for tool/skill calls from thread context loop = asyncio.get_running_loop() if "tools" in self._namespace: self._namespace["tools"].set_loop(loop) + if "skills" in self._namespace: + self._namespace["skills"].set_loop(loop) # Run in thread to allow timeout cancellation try: diff --git a/src/py_code_mode/execution/in_process/skills_namespace.py b/src/py_code_mode/execution/in_process/skills_namespace.py index e86f1ff..d72134e 100644 --- a/src/py_code_mode/execution/in_process/skills_namespace.py +++ b/src/py_code_mode/execution/in_process/skills_namespace.py @@ -6,7 +6,9 @@ from __future__ import annotations +import asyncio import builtins +import inspect from typing import TYPE_CHECKING, Any from py_code_mode.skills import SkillLibrary @@ -44,6 +46,15 @@ def __init__(self, library: SkillLibrary, namespace: dict[str, Any]) -> None: self._library = library self._namespace = namespace + self._loop: asyncio.AbstractEventLoop | None = None + + def set_loop(self, loop: asyncio.AbstractEventLoop) -> None: + """Set the event loop to use for async skill invocations. + + When code runs in a thread (via asyncio.to_thread), we need a reference + to the main event loop to execute async skills via run_coroutine_threadsafe. + """ + self._loop = loop @property def library(self) -> SkillLibrary: @@ -143,8 +154,6 @@ def invoke(self, skill_name: str, **kwargs: Any) -> Any: if skill is None: raise ValueError(f"Skill not found: {skill_name}") - # Execute skill source fresh, same as regular code execution - # Create isolated namespace with copies of tools/skills/artifacts refs skill_namespace = { "tools": self._namespace.get("tools"), "skills": self._namespace.get("skills"), @@ -152,4 +161,12 @@ def invoke(self, skill_name: str, **kwargs: Any) -> Any: } code = compile(skill.source, f"", "exec") _run_code(code, skill_namespace) - return skill_namespace["run"](**kwargs) + result = skill_namespace["run"](**kwargs) + + if inspect.iscoroutine(result): + if self._loop is not None: + future = asyncio.run_coroutine_threadsafe(result, self._loop) + return future.result() + return asyncio.run(result) + + return result diff --git a/src/py_code_mode/execution/subprocess/config.py b/src/py_code_mode/execution/subprocess/config.py index 4073a3c..cfcfa96 100644 --- a/src/py_code_mode/execution/subprocess/config.py +++ b/src/py_code_mode/execution/subprocess/config.py @@ -49,7 +49,7 @@ class SubprocessConfig: deps_file: Path to requirements.txt-style file for pre-configured deps. None means no deps file. ipc_timeout: Timeout for IPC queries (tool/skill/artifact) in seconds. - Default: 30.0. + None means unlimited (default). """ python_version: str | None = None @@ -63,7 +63,7 @@ class SubprocessConfig: tools_path: Path | None = None deps: tuple[str, ...] | None = None deps_file: Path | None = None - ipc_timeout: float = 30.0 + ipc_timeout: float | None = None def __post_init__(self) -> None: """Validate configuration values.""" diff --git a/src/py_code_mode/execution/subprocess/kernel_init.py b/src/py_code_mode/execution/subprocess/kernel_init.py index 58ccdbd..c481d24 100644 --- a/src/py_code_mode/execution/subprocess/kernel_init.py +++ b/src/py_code_mode/execution/subprocess/kernel_init.py @@ -12,7 +12,7 @@ from __future__ import annotations -def get_kernel_init_code(ipc_timeout: float = 30.0) -> str: +def get_kernel_init_code(ipc_timeout: float | None = None) -> str: """Generate kernel initialization code with configurable timeout. Args: @@ -236,12 +236,11 @@ def _rpc_call(method: str, **params) -> Any: ident=parent_ident, ) - # Wait for response with timeout - timeout_seconds = _RPC_TIMEOUT + # Wait for response (with optional timeout) elapsed = 0.0 poll_interval = 0.01 - while elapsed < timeout_seconds: + while _RPC_TIMEOUT is None or elapsed < _RPC_TIMEOUT: try: rlist, _, xlist = zmq.select( [kernel.stdin_socket], [], [kernel.stdin_socket], poll_interval @@ -256,7 +255,7 @@ def _rpc_call(method: str, **params) -> Any: pass # Timeout or socket error, continue polling elapsed += poll_interval else: - raise TimeoutError(f"RPC call {{method}} timed out after {{timeout_seconds}}s") + raise TimeoutError(f"RPC call {{method}} timed out after {{_RPC_TIMEOUT}}s") # Parse response try: @@ -419,6 +418,7 @@ def invoke(self, skill_name: str, **kwargs) -> Any: Gets skill source from host and executes it locally in the kernel. This ensures skills can import packages installed at runtime. + Handles async skills by running them with asyncio.run(). Args: skill_name: Name of the skill to invoke. @@ -427,7 +427,8 @@ def invoke(self, skill_name: str, **kwargs) -> Any: Note: Uses skill_name (not name) to avoid collision with skills that have a 'name' parameter. """ - # Get skill source from host (storage access) + import asyncio + skill = _rpc_call("skills.get", name=skill_name) if skill is None: raise ValueError(f"Skill not found: {{skill_name}}") @@ -436,7 +437,6 @@ def invoke(self, skill_name: str, **kwargs) -> Any: if not source: raise ValueError(f"Skill has no source: {{skill_name}}") - # Execute skill locally in kernel with access to namespaces skill_namespace = {{ "tools": tools, "skills": skills, @@ -446,12 +446,25 @@ def invoke(self, skill_name: str, **kwargs) -> Any: code = compile(source, f"", "exec") exec(code, skill_namespace) - # Call the run function run_func = skill_namespace.get("run") if not callable(run_func): raise ValueError(f"Skill {{skill_name}} has no run() function") - return run_func(**kwargs) + result = run_func(**kwargs) + if asyncio.iscoroutine(result): + try: + asyncio.get_running_loop() + has_loop = True + except RuntimeError: + has_loop = False + + if has_loop: + import concurrent.futures + with concurrent.futures.ThreadPoolExecutor() as pool: + future = pool.submit(asyncio.run, result) + return future.result() + return asyncio.run(result) + return result def search(self, query: str, limit: int = 5) -> list[Skill]: """Search for skills matching query. @@ -650,4 +663,4 @@ def __repr__(self) -> str: # For backward compatibility, provide the raw code string -KERNEL_INIT_CODE = get_kernel_init_code(ipc_timeout=30.0) +KERNEL_INIT_CODE = get_kernel_init_code(ipc_timeout=None) diff --git a/src/py_code_mode/skills/skill.py b/src/py_code_mode/skills/skill.py index 0220c63..aaa380c 100644 --- a/src/py_code_mode/skills/skill.py +++ b/src/py_code_mode/skills/skill.py @@ -149,15 +149,19 @@ def from_source( except SyntaxError as e: raise SyntaxError(f"Syntax error in skill code: {e}") - # Check for run() function definition - has_run_func = False + has_async_run = False + has_sync_run = False for node in ast.walk(tree): - if isinstance(node, ast.FunctionDef) and node.name == "run": - has_run_func = True + if isinstance(node, ast.AsyncFunctionDef) and node.name == "run": + has_async_run = True break + if isinstance(node, ast.FunctionDef) and node.name == "run": + has_sync_run = True - if not has_run_func: - raise ValueError("Skill must define a run() function") + if has_sync_run and not has_async_run: + raise ValueError("Skill must define 'async def run()', not 'def run()'") + if not has_async_run: + raise ValueError("Skill must define an 'async def run()' function") # Compile and execute to get the function namespace: dict[str, Any] = {} @@ -194,13 +198,32 @@ def from_source( def from_file(cls, path: Path) -> PythonSkill: """Load a Python skill from a .py file. - The file must have a run() function as entrypoint. + The file must have an async def run() function as entrypoint. Parameters are extracted from the function signature. Description comes from the module or function docstring. """ - # Read source for agent inspection source = path.read_text() + # Validate async def run() requirement + try: + tree = ast.parse(source) + except SyntaxError as e: + raise SyntaxError(f"Syntax error in skill {path}: {e}") + + has_async_run = False + has_sync_run = False + for node in ast.walk(tree): + if isinstance(node, ast.AsyncFunctionDef) and node.name == "run": + has_async_run = True + break + if isinstance(node, ast.FunctionDef) and node.name == "run": + has_sync_run = True + + if has_sync_run and not has_async_run: + raise ValueError(f"Skill {path} must define 'async def run()', not 'def run()'") + if not has_async_run: + raise ValueError(f"Skill {path} must define an 'async def run()' function") + # Load module dynamically spec = importlib.util.spec_from_file_location(path.stem, path) if spec is None or spec.loader is None: @@ -209,10 +232,6 @@ def from_file(cls, path: Path) -> PythonSkill: module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) - # Get run() function - if not hasattr(module, "run"): - raise ValueError(f"Skill {path} must have a run() function") - func = module.run # Extract description from module or function docstring @@ -234,12 +253,12 @@ def from_file(cls, path: Path) -> PythonSkill: ), ) - def invoke(self, **kwargs: Any) -> Any: + async def invoke(self, **kwargs: Any) -> Any: """Invoke the skill with given parameters. - Calls the run() function directly. + Awaits the async run() function. """ - return self._func(**kwargs) + return await self._func(**kwargs) @property def tags(self) -> frozenset[str]: diff --git a/tests/conftest.py b/tests/conftest.py index ab801a0..86f694b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -743,7 +743,7 @@ def sample_skill_source() -> str: """Sample skill source code for testing.""" return '''"""Double a number.""" -def run(n: int) -> int: +async def run(n: int) -> int: return n * 2 ''' diff --git a/tests/container/test_container_api.py b/tests/container/test_container_api.py index 9671b19..5bea730 100644 --- a/tests/container/test_container_api.py +++ b/tests/container/test_container_api.py @@ -164,7 +164,7 @@ def test_create_skill_success(self, auth_client) -> None: "/api/skills", json={ "name": "test_skill", - "source": "def run(x: int) -> int:\n return x * 2", + "source": "async def run(x: int) -> int:\n return x * 2", "description": "Doubles a number", }, headers={"Authorization": f"Bearer {token}"}, @@ -182,7 +182,7 @@ def test_create_skill_requires_auth(self, auth_client) -> None: "/api/skills", json={ "name": "test_skill", - "source": "def run(): pass", + "source": "async def run(): pass", "description": "Test", }, ) @@ -238,11 +238,14 @@ def test_skill_lifecycle_create_get_delete(self, auth_client) -> None: headers = {"Authorization": f"Bearer {token}"} # Create + skill_source = ( + 'async def run(n: int) -> int:\n """Square a number."""\n return n ** 2' + ) response = client.post( "/api/skills", json={ "name": "lifecycle_skill", - "source": 'def run(n: int) -> int:\n """Square a number."""\n return n ** 2', + "source": skill_source, "description": "Squares a number", }, headers=headers, diff --git a/tests/test_backend_integration.py b/tests/test_backend_integration.py index bec7837..75cc577 100644 --- a/tests/test_backend_integration.py +++ b/tests/test_backend_integration.py @@ -61,7 +61,7 @@ def skills_dir(self, tmp_path: Path) -> Path: (skills / "double.py").write_text( '''"""Double a number.""" -def run(n: int) -> int: +async def run(n: int) -> int: return n * 2 ''' ) @@ -305,7 +305,7 @@ def executor_with_skills(self, tmp_path: Path) -> InProcessExecutor: # Create a test skill using from_source skill = PythonSkill.from_source( name="double", - source='def run(n: int) -> int:\n """Double a number."""\n return n * 2', + source='async def run(n: int) -> int:\n """Double a number."""\n return n * 2', description="Double a number", ) @@ -341,7 +341,7 @@ async def test_skill_with_default_args(self, tmp_path: Path) -> None: skills_path.mkdir() source = ( - 'def run(name: str = "World") -> str:\n' + 'async def run(name: str = "World") -> str:\n' ' """Greet someone."""\n' ' return f"Hello, {name}!"' ) diff --git a/tests/test_backend_user_journey.py b/tests/test_backend_user_journey.py index af43027..4de3380 100644 --- a/tests/test_backend_user_journey.py +++ b/tests/test_backend_user_journey.py @@ -132,7 +132,7 @@ async def test_agent_full_workflow_in_process( skills.create( name="shout", description="Echo text in uppercase", - source="def run(text: str) -> str:\\n return text.upper()" + source="async def run(text: str) -> str:\\n return text.upper()" ) """) assert result.is_ok, f"skills.create() failed: {result.error}" @@ -187,7 +187,7 @@ async def test_agent_full_workflow_container( skills.create( name="container_shout", description="Echo text in uppercase", - source="def run(text: str) -> str:\\n return text.upper()" + source="async def run(text: str) -> str:\\n return text.upper()" ) """) assert result.is_ok, f"skills.create() failed: {result.error}" @@ -290,7 +290,7 @@ async def test_skills_create_invoke_delete(self, empty_storage: Path) -> None: skills.create( name="add_numbers", description="Add two numbers", - source="def run(a: int, b: int) -> int:\\n return a + b" + source="async def run(a: int, b: int) -> int:\\n return a + b" ) """) assert result.is_ok, f"skills.create() failed: {result.error}" @@ -333,7 +333,7 @@ async def test_skill_uses_tools_in_container( skills.create( name="loud_echo", description="Echo text and uppercase it", - source="""def run(text: str) -> str: + source="""async def run(text: str) -> str: result = tools.echo.echo(text=text) return result.strip().upper() """ @@ -369,7 +369,7 @@ async def test_skill_persists_across_container_sessions(self, empty_storage: Pat skills.create( name="persistent_skill", description="Should persist", - source="def run() -> str:\\n return 'persisted'" + source="async def run() -> str:\\n return 'persisted'" ) """) assert result.is_ok @@ -443,7 +443,7 @@ async def test_redis_container_full_workflow(self, redis_url: str) -> None: skills.create( name="redis_skill", description="Test skill in Redis", - source="def run(x: int) -> int:\\n return x * 2" + source="async def run(x: int) -> int:\\n return x * 2" ) """) assert result.is_ok, f"skills.create() failed: {result.error}" @@ -479,7 +479,7 @@ async def test_redis_container_skill_persistence(self, redis_url: str) -> None: skills.create( name="redis_persistent", description="Should persist in Redis", - source="def run() -> str:\\n return 'from redis'" + source="async def run() -> str:\\n return 'from redis'" ) """) assert result.is_ok @@ -512,7 +512,7 @@ async def test_redis_container_skill_search(self, redis_url: str) -> None: skills.create( name="port_scanner", description="Scan network ports to find open services", - source="def run(host: str) -> list:\\n return ['port scanning', host]" + source="async def run(host: str) -> list:\\n return ['port scanning', host]" ) """) assert result.is_ok, f"skills.create() failed: {result.error}" @@ -539,7 +539,7 @@ async def test_redis_search_skills_facade(self, redis_url: str) -> None: # 1. Create a skill using facade method await session.add_skill( name="web_scraper", - source="def run(url: str) -> str:\n return f'scraped {url}'", + source="async def run(url: str) -> str:\n return f'scraped {url}'", description="Scrape web pages and extract content", ) @@ -603,7 +603,7 @@ async def test_redis_stack_search_skills_facade(self, redis_stack_url: str) -> N # 1. Create a skill await session.add_skill( name="data_analyzer", - source="def run(data: list) -> dict:\n return {'count': len(data)}", + source="async def run(data: list) -> dict:\n return {'count': len(data)}", description="Analyze data and return statistics", ) @@ -674,7 +674,7 @@ async def test_container_invalid_skill_source_rejected(self, empty_storage: Path skills.create( name="bad_skill", description="Invalid syntax", - source="def run( broken" + source="async def run( broken" ) """) assert not result.is_ok, "Expected error for invalid syntax" diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index ada5c6e..b0aa938 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -695,7 +695,9 @@ async def test_config_roundtrip(self, tmp_path: Path) -> None: storage = FileStorage(tmp_path) (tmp_path / "skills").mkdir(exist_ok=True) skill_file = tmp_path / "skills" / "greet.py" - skill_content = '"""Greet."""\ndef run(name: str) -> str:\n return f"Hello, {name}!"' + skill_content = ( + '"""Greet."""\nasync def run(name: str) -> str:\n return f"Hello, {name}!"' + ) skill_file.write_text(skill_content) # Serialize @@ -852,7 +854,7 @@ async def test_config_roundtrip(self, mock_redis: MockRedisClient) -> None: skill_store = storage.get_skill_store() test_skill = PythonSkill.from_source( name="greet", - source='def run(name: str) -> str:\n return f"Hello, {name}!"', + source='async def run(name: str) -> str:\n return f"Hello, {name}!"', description="Greet a user", ) skill_store.save(test_skill) @@ -1117,7 +1119,8 @@ async def test_file_storage_bootstrap_journey(self, tmp_path: Path) -> None: storage = FileStorage(tmp_path) (tmp_path / "skills").mkdir(exist_ok=True) skill_file = tmp_path / "skills" / "double.py" - skill_file.write_text('"""Double a number."""\ndef run(n: int) -> int:\n return n * 2') + skill_content = '"""Double a number."""\nasync def run(n: int) -> int:\n return n * 2' + skill_file.write_text(skill_content) (tmp_path / "artifacts").mkdir(exist_ok=True) @@ -1166,7 +1169,7 @@ async def test_redis_storage_bootstrap_journey(self, mock_redis: MockRedisClient skill_store = storage.get_skill_store() skill = PythonSkill.from_source( name="triple", - source="def run(n: int) -> int:\n return n * 3", + source="async def run(n: int) -> int:\n return n * 3", description="Triple a number", ) skill_store.save(skill) diff --git a/tests/test_chroma_vector_store.py b/tests/test_chroma_vector_store.py index 737ca8c..76b6293 100644 --- a/tests/test_chroma_vector_store.py +++ b/tests/test_chroma_vector_store.py @@ -141,7 +141,7 @@ def test_detects_model_change_different_dimension( store1.add( id="skill1", description="Test skill", - source="def run(): pass", + source="async def run(): pass", content_hash="abc123", ) assert store1.count() == 1 @@ -166,7 +166,7 @@ def test_preserves_vectors_when_model_unchanged( store1.add( id="skill1", description="Test skill", - source="def run(): pass", + source="async def run(): pass", content_hash="abc123", ) assert store1.count() == 1 @@ -200,7 +200,7 @@ def test_clears_all_vectors_on_model_change( store1.add( id=f"skill{i}", description=f"Skill {i}", - source=f"def run(): return {i}", + source=f"async def run(): return {i}", content_hash=f"hash{i}", ) assert store1.count() == 5 @@ -235,7 +235,7 @@ def test_add_embeds_and_stores_vectors(self, store) -> None: store.add( id="port_scanner", description="Scan network ports using nmap", - source='def run(target: str):\n return subprocess.run(["nmap", target])', + source='async def run(target: str):\n return subprocess.run(["nmap", target])', content_hash="abc123def456", ) @@ -247,7 +247,7 @@ def test_add_stores_content_hash(self, store) -> None: store.add( id="skill1", description="Test skill", - source="def run(): pass", + source="async def run(): pass", content_hash="contenthash123", ) @@ -265,7 +265,7 @@ def test_add_overwrites_existing_skill(self, store) -> None: store.add( id="skill1", description="Original description", - source="def run(): return 1", + source="async def run(): return 1", content_hash="hash1", ) assert store.count() == 1 @@ -273,7 +273,7 @@ def test_add_overwrites_existing_skill(self, store) -> None: store.add( id="skill1", description="Updated description", - source="def run(): return 2", + source="async def run(): return 2", content_hash="hash2", ) @@ -288,7 +288,7 @@ def test_remove_deletes_skill_vectors(self, store) -> None: store.add( id="skill1", description="Test", - source="def run(): pass", + source="async def run(): pass", content_hash="hash1", ) assert store.count() == 1 @@ -527,7 +527,7 @@ def test_same_content_hash_skips_re_embedding( store.add( id="skill1", description="Test skill", - source="def run(): pass", + source="async def run(): pass", content_hash="stable_hash", ) initial_embed_count = embedder_with_call_tracking.embed_call_count @@ -536,7 +536,7 @@ def test_same_content_hash_skips_re_embedding( store.add( id="skill1", description="Test skill", - source="def run(): pass", + source="async def run(): pass", content_hash="stable_hash", ) @@ -556,7 +556,7 @@ def test_different_content_hash_triggers_re_embedding( store.add( id="skill1", description="Original description", - source="def run(): return 1", + source="async def run(): return 1", content_hash="hash_v1", ) initial_embed_count = embedder_with_call_tracking.embed_call_count @@ -565,7 +565,7 @@ def test_different_content_hash_triggers_re_embedding( store.add( id="skill1", description="Updated description", - source="def run(): return 2", + source="async def run(): return 2", content_hash="hash_v2", ) diff --git a/tests/test_error_handling.py b/tests/test_error_handling.py index 4b291b3..cd966d0 100644 --- a/tests/test_error_handling.py +++ b/tests/test_error_handling.py @@ -88,14 +88,14 @@ def skills_dir_with_corruption(tmp_path: Path) -> Path: # Valid skill (skills_dir / "valid_skill.py").write_text('''"""A valid skill.""" -def run(x: int) -> int: +async def run(x: int) -> int: return x * 2 ''') # Corrupted Python (syntax error) (skills_dir / "syntax_error.py").write_text('''"""Skill with syntax error.""" -def run(x: int) -> int +async def run(x: int) -> int return x * 2 # Missing colon above ''') @@ -120,7 +120,7 @@ def __init__(self): "valid": json.dumps( { "name": "valid", - "source": "def run(): pass", + "source": "async def run(): pass", "description": "Valid skill", } ).encode(), diff --git a/tests/test_executor_configs.py b/tests/test_executor_configs.py index 908c7fd..0fc884f 100644 --- a/tests/test_executor_configs.py +++ b/tests/test_executor_configs.py @@ -211,16 +211,16 @@ def test_deps_file_accepts_path(self, tmp_path: Path) -> None: config = SubprocessConfig(deps_file=deps_file) assert config.deps_file == deps_file - def test_ipc_timeout_default_is_30(self) -> None: - """ipc_timeout defaults to 30.0 seconds. + def test_ipc_timeout_default_is_none(self) -> None: + """ipc_timeout defaults to None (unlimited). - Contract: SubprocessConfig().ipc_timeout == 30.0 + Contract: SubprocessConfig().ipc_timeout is None Breaks when: Default changed or field missing. """ from py_code_mode.execution import SubprocessConfig config = SubprocessConfig() - assert config.ipc_timeout == 30.0 + assert config.ipc_timeout is None def test_ipc_timeout_accepts_custom_value(self) -> None: """ipc_timeout accepts a custom value. @@ -464,23 +464,15 @@ def test_all_configs_have_deps_file(self) -> None: def test_all_configs_have_ipc_timeout(self) -> None: """All executor configs have ipc_timeout field. - Contract: Consistent API across all executors. - Breaks when: Any config missing the field. + Contract: All configs support ipc_timeout. + Breaks when: Any config missing ipc_timeout. + Note: SubprocessConfig defaults to None (unlimited), others to 30.0. """ from py_code_mode.execution import ContainerConfig, InProcessConfig, SubprocessConfig assert hasattr(InProcessConfig(), "ipc_timeout") assert hasattr(SubprocessConfig(), "ipc_timeout") assert hasattr(ContainerConfig(), "ipc_timeout") - - def test_all_configs_ipc_timeout_default_30(self) -> None: - """All executor configs have ipc_timeout default of 30.0. - - Contract: Consistent default across all executors. - Breaks when: Any config has different default. - """ - from py_code_mode.execution import ContainerConfig, InProcessConfig, SubprocessConfig - assert InProcessConfig().ipc_timeout == 30.0 - assert SubprocessConfig().ipc_timeout == 30.0 + assert SubprocessConfig().ipc_timeout is None assert ContainerConfig().ipc_timeout == 30.0 diff --git a/tests/test_feature_matrix_comprehensive.py b/tests/test_feature_matrix_comprehensive.py index 442900a..4abfc0a 100644 --- a/tests/test_feature_matrix_comprehensive.py +++ b/tests/test_feature_matrix_comprehensive.py @@ -99,7 +99,7 @@ def populated_dir(tmp_path: Path) -> tuple[Path, Path]: # Sample skill (skills_dir / "double.py").write_text('''"""Double a number.""" -def run(n: int) -> int: +async def run(n: int) -> int: return n * 2 ''') @@ -182,7 +182,7 @@ async def test_complete_workflow_from_empty_directory(self, empty_base_dir: Path skills.create( name="triple", description="Triple a number", - source="def run(n: int) -> int:\\n return n * 3" + source="async def run(n: int) -> int:\\n return n * 3" ) """) assert result.is_ok, f"skills.create() failed: {result.error}" @@ -239,7 +239,7 @@ async def test_skills_persist_across_sessions(self, empty_base_dir: Path) -> Non skills.create( name="quadruple", description="Multiply by 4", - source="def run(n: int) -> int:\\n return n * 4" + source="async def run(n: int) -> int:\\n return n * 4" ) """) assert result.is_ok, f"skills.create() failed: {result.error}" @@ -344,7 +344,7 @@ async def test_skills_create_creates_directory(self, empty_base_dir: Path) -> No skills.create( name="test_skill", description="Test", - source="def run() -> str:\\n return 'ok'" + source="async def run() -> str:\\n return 'ok'" ) """) assert result.is_ok, f"Failed: {result.error}" @@ -524,7 +524,7 @@ async def test_skills_work_without_tools_directory(self, partial_dir_skills_only (partial_dir_skills_only / "skills" / "add.py").write_text(''' """Add two numbers.""" -def run(a: int, b: int) -> int: +async def run(a: int, b: int) -> int: return a + b ''') @@ -580,7 +580,7 @@ async def test_skills_create_and_persist_in_redis(self, redis_storage) -> None: skills.create( name="redis_skill", description="Test skill", - source="def run() -> str:\\n return 'from redis'" + source="async def run() -> str:\\n return 'from redis'" ) """) assert result.is_ok, f"skills.create() failed: {result.error}" @@ -730,7 +730,7 @@ async def test_skills_create_feature(self, storage_and_executor) -> None: skills.create( name="matrix_test", description="Matrix test skill", - source="def run() -> str:\\n return 'matrix'" + source="async def run() -> str:\\n return 'matrix'" ) """) assert result.is_ok, f"skills.create() failed: {result.error}" diff --git a/tests/test_integration.py b/tests/test_integration.py index 5addc26..b1cd92b 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -382,7 +382,7 @@ def skill_library(self, tmp_path: Path) -> SkillLibrary: # Create a simple skill (skills_path / "double.py").write_text('''"""Double a number.""" -def run(n: int) -> int: +async def run(n: int) -> int: return n * 2 ''') diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index 27d5aff..10fb5f9 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -90,13 +90,13 @@ def mcp_storage_dir(tmp_path: Path) -> tuple[Path, Path]: # Simple skill for basic testing (skills_dir / "double.py").write_text('''"""Double a number.""" -def run(n: int) -> int: +async def run(n: int) -> int: return n * 2 ''') # Skill that calls tools (for cross-namespace testing) (skills_dir / "fetch_title.py").write_text('''"""Fetch a URL and extract title.""" -def run(url: str) -> str: +async def run(url: str) -> str: import re content = tools.curl.get(url=url) match = re.search(r'([^<]+)', content, re.I) @@ -274,7 +274,7 @@ async def test_mcp_server_skill_create_and_invoke( skills.create( name="add_numbers", source=""" -def run(a: int, b: int) -> int: +async def run(a: int, b: int) -> int: return a + b """, description="Add two numbers together" @@ -314,7 +314,7 @@ async def test_mcp_server_skill_persists_across_calls( "code": """ skills.create( name="triple", - source="def run(n: int) -> int:\\n return n * 3", + source="async def run(n: int) -> int:\\n return n * 3", description="Triple a number" ) """ @@ -360,7 +360,7 @@ async def test_mcp_server_skill_delete( "code": """ skills.create( name="temp_skill", - source="def run() -> str:\\n return 'temporary'", + source="async def run() -> str:\\n return 'temporary'", description="Temporary skill for deletion test" ) """ @@ -598,7 +598,7 @@ async def test_mcp_server_skill_calls_tool( skills.create( name="shout", source=""" -def run(message: str) -> str: +async def run(message: str) -> str: return tools.echo(text=message.upper()) """, description="Echo message in uppercase" @@ -667,7 +667,7 @@ async def test_mcp_server_skill_calls_another_skill( skills.create( name="quadruple", source=""" -def run(n: int) -> int: +async def run(n: int) -> int: doubled = skills.invoke("double", n=n) return skills.invoke("double", n=doubled) """, @@ -851,7 +851,7 @@ async def test_mcp_server_create_skill( await session.initialize() # Create skill via dedicated MCP tool (not run_code) - skill_source = "def run(x: int, y: int) -> int:\n return x + y\n" + skill_source = "async def run(x: int, y: int) -> int:\n return x + y\n" result = await session.call_tool( "create_skill", { @@ -892,7 +892,7 @@ async def test_mcp_server_create_skill_persists( await session.initialize() # Create skill via dedicated MCP tool - skill_source = "def run(text: str) -> str:\n return text.upper()\n" + skill_source = "async def run(text: str) -> str:\n return text.upper()\n" await session.call_tool( "create_skill", { @@ -934,7 +934,7 @@ async def test_mcp_server_delete_skill( await session.initialize() # Create a skill via MCP tool - skill_source = "def run() -> str:\n return 'I exist'\n" + skill_source = "async def run() -> str:\n return 'I exist'\n" await session.call_tool( "create_skill", { @@ -1042,7 +1042,7 @@ async def test_mcp_server_full_workflow( name="sum_csv", source=""" import re -def run(text: str) -> int: +async def run(text: str) -> int: numbers = [int(x) for x in re.findall(r'\\\\d+', text)] return sum(numbers) """, diff --git a/tests/test_negative.py b/tests/test_negative.py index ce83132..fdb9ad9 100644 --- a/tests/test_negative.py +++ b/tests/test_negative.py @@ -187,7 +187,7 @@ def storage(self, tmp_path: Path) -> FileStorage: (skills_dir / "divide.py").write_text( '''"""Divide two numbers.""" -def run(a: int, b: int) -> float: +async def run(a: int, b: int) -> float: return a / b ''' ) @@ -235,7 +235,7 @@ async def test_skill_create_invalid_source_error(self, storage: FileStorage) -> skills.create( name="bad", description="Invalid skill", - source="def run( INVALID SYNTAX" + source="async def run( INVALID SYNTAX" ) """ ) @@ -372,7 +372,7 @@ def test_corrupted_skill_handling(self, tmp_path: Path) -> None: """FileStorage handles corrupted skill files.""" skills_dir = tmp_path / "skills" skills_dir.mkdir() - (skills_dir / "bad.py").write_text("def run( INVALID") + (skills_dir / "bad.py").write_text("async def run( INVALID") storage = FileStorage(tmp_path) diff --git a/tests/test_redis_vector_store.py b/tests/test_redis_vector_store.py index 4018ad4..813a443 100644 --- a/tests/test_redis_vector_store.py +++ b/tests/test_redis_vector_store.py @@ -163,7 +163,7 @@ def test_reuses_existing_compatible_index( prefix="skills", index_name="skills_idx", ) - store1.add("skill1", "Test skill", "def run(): pass", "hash1") + store1.add("skill1", "Test skill", "async def run(): pass", "hash1") assert store1.count() == 1 # Create second store with same config - should reuse index @@ -218,7 +218,7 @@ def test_detects_model_change_different_dimension( store1.add( id="skill1", description="Test skill", - source="def run(): pass", + source="async def run(): pass", content_hash="abc123", ) assert store1.count() == 1 @@ -252,7 +252,7 @@ def test_preserves_vectors_when_model_unchanged( store1.add( id="skill1", description="Test skill", - source="def run(): pass", + source="async def run(): pass", content_hash="abc123", ) assert store1.count() == 1 @@ -292,7 +292,7 @@ def test_clears_all_vectors_on_model_change( store1.add( id=f"skill{i}", description=f"Skill {i}", - source=f"def run(): return {i}", + source=f"async def run(): return {i}", content_hash=f"hash{i}", ) assert store1.count() == 5 @@ -337,7 +337,7 @@ def test_add_embeds_and_stores_vectors(self, store) -> None: store.add( id="port_scanner", description="Scan network ports using nmap", - source='def run(target: str):\n return subprocess.run(["nmap", target])', + source='async def run(target: str):\n return subprocess.run(["nmap", target])', content_hash="abc123def456", ) @@ -349,7 +349,7 @@ def test_add_stores_content_hash(self, store) -> None: store.add( id="skill1", description="Test skill", - source="def run(): pass", + source="async def run(): pass", content_hash="contenthash123", ) @@ -367,7 +367,7 @@ def test_add_overwrites_existing_skill(self, store) -> None: store.add( id="skill1", description="Original description", - source="def run(): return 1", + source="async def run(): return 1", content_hash="hash1", ) assert store.count() == 1 @@ -375,7 +375,7 @@ def test_add_overwrites_existing_skill(self, store) -> None: store.add( id="skill1", description="Updated description", - source="def run(): return 2", + source="async def run(): return 2", content_hash="hash2", ) @@ -390,7 +390,7 @@ def test_remove_deletes_skill_vectors(self, store) -> None: store.add( id="skill1", description="Test", - source="def run(): pass", + source="async def run(): pass", content_hash="hash1", ) assert store.count() == 1 @@ -645,7 +645,7 @@ def test_same_content_hash_skips_re_embedding( store.add( id="skill1", description="Test skill", - source="def run(): pass", + source="async def run(): pass", content_hash="stable_hash", ) initial_embed_count = embedder_with_call_tracking.embed_call_count @@ -654,7 +654,7 @@ def test_same_content_hash_skips_re_embedding( store.add( id="skill1", description="Test skill", - source="def run(): pass", + source="async def run(): pass", content_hash="stable_hash", ) @@ -679,7 +679,7 @@ def test_different_content_hash_triggers_re_embedding( store.add( id="skill1", description="Original description", - source="def run(): return 1", + source="async def run(): return 1", content_hash="hash_v1", ) initial_embed_count = embedder_with_call_tracking.embed_call_count @@ -688,7 +688,7 @@ def test_different_content_hash_triggers_re_embedding( store.add( id="skill1", description="Updated description", - source="def run(): return 2", + source="async def run(): return 2", content_hash="hash_v2", ) diff --git a/tests/test_rpc_errors.py b/tests/test_rpc_errors.py index bacef2e..311669d 100644 --- a/tests/test_rpc_errors.py +++ b/tests/test_rpc_errors.py @@ -81,7 +81,7 @@ async def test_skill_with_missing_import_raises_skill_error( create_code = ''' skills.create( "broken_skill", - """def run(): + """async def run(): import this_module_definitely_does_not_exist return "never reached" """, @@ -233,7 +233,7 @@ async def test_error_preserves_original_exception_type( create_code = ''' skills.create( "value_error_skill", - """def run(): + """async def run(): raise ValueError("intentional value error") """, "A skill that raises ValueError" diff --git a/tests/test_semantic.py b/tests/test_semantic.py index c35f653..f812e61 100644 --- a/tests/test_semantic.py +++ b/tests/test_semantic.py @@ -9,7 +9,7 @@ def _make_skill(name: str, description: str, code: str) -> PythonSkill: """Helper to create a PythonSkill from minimal info.""" - source = f'"""{description}"""\n\ndef run():\n {code}' + source = f'"""{description}"""\n\nasync def run():\n {code}' return PythonSkill.from_source(name=name, source=source, description=description) @@ -128,7 +128,7 @@ def python_skill(self) -> PythonSkill: source = dedent(''' """Calculate sum of numbers.""" - def run(numbers: list[int]) -> int: + async def run(numbers: list[int]) -> int: return sum(numbers) ''').strip() return PythonSkill.from_source(name="sum_numbers", source=source) diff --git a/tests/test_session.py b/tests/test_session.py index dcf6955..aaf62b7 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -170,7 +170,7 @@ def storage_with_skills(self, tmp_path: Path) -> FileStorage: (skills_dir / "double.py").write_text( '''"""Double a number.""" -def run(n: int) -> int: +async def run(n: int) -> int: return n * 2 ''' ) @@ -217,7 +217,7 @@ async def test_skills_create_callable(self, storage_with_skills: FileStorage) -> skills.create( name="triple", description="Triple a number", - source="def run(n: int) -> int:\\n return n * 3" + source="async def run(n: int) -> int:\\n return n * 3" ) """ ) @@ -593,7 +593,7 @@ def storage_with_skills(self, tmp_path: Path) -> FileStorage: (skills_dir / "double.py").write_text( '''"""Double a number.""" -def run(n: int) -> int: +async def run(n: int) -> int: return n * 2 ''' ) @@ -908,7 +908,7 @@ async def test_subprocess_namespaces_work_via_session(self, tmp_path: Path) -> N (skills_dir / "add_numbers.py").write_text( '''"""Add two numbers together.""" -def run(a: int, b: int) -> int: +async def run(a: int, b: int) -> int: return a + b ''' ) diff --git a/tests/test_skill_library_vector_store.py b/tests/test_skill_library_vector_store.py index 4daeced..d86c346 100644 --- a/tests/test_skill_library_vector_store.py +++ b/tests/test_skill_library_vector_store.py @@ -18,7 +18,7 @@ def _make_skill(name: str, description: str, code: str) -> PythonSkill: """Helper to create a PythonSkill from minimal info.""" - source = f'"""{description}"""\n\ndef run():\n {code}' + source = f'"""{description}"""\n\nasync def run():\n {code}' return PythonSkill.from_source(name=name, source=source, description=description) diff --git a/tests/test_skill_store.py b/tests/test_skill_store.py index acfdcb0..e4a19fc 100644 --- a/tests/test_skill_store.py +++ b/tests/test_skill_store.py @@ -20,7 +20,7 @@ def sample_python_skill() -> PythonSkill: """A simple Python skill for testing.""" return PythonSkill.from_source( name="greet", - source='def run(name: str) -> str:\n return f"Hello, {name}!"', + source='async def run(name: str) -> str:\n return f"Hello, {name}!"', description="Greet someone", ) @@ -30,7 +30,7 @@ def another_python_skill() -> PythonSkill: """Another Python skill for testing list operations.""" return PythonSkill.from_source( name="farewell", - source='def run() -> str:\n return "Goodbye!"', + source='async def run() -> str:\n return "Goodbye!"', description="Say goodbye", ) @@ -139,7 +139,7 @@ def test_save_overwrites_existing( updated = PythonSkill.from_source( name="greet", - source='def run(name: str) -> str:\n return f"Hi, {name}!"', + source='async def run(name: str) -> str:\n return f"Hi, {name}!"', description="Updated greeting", ) memory_store.save(updated) @@ -204,7 +204,7 @@ def test_list_all_finds_py_files( # Create another skill another = PythonSkill.from_source( name="farewell", - source='def run() -> str:\n return "Goodbye!"', + source='async def run() -> str:\n return "Goodbye!"', description="Say goodbye", ) file_store.save(another) @@ -219,7 +219,7 @@ def test_list_all_ignores_underscore_files(self, file_store: FileSkillStore, tmp """Should skip files starting with underscore.""" # Create __init__.py (tmp_path / "__init__.py").write_text("") - (tmp_path / "_private.py").write_text("def run(): pass") + (tmp_path / "_private.py").write_text("async def run(): pass") skills = file_store.list_all() assert len(skills) == 0 @@ -313,7 +313,7 @@ def test_list_all(self, redis_store: RedisSkillStore, sample_python_skill: Pytho another = PythonSkill.from_source( name="farewell", - source='def run() -> str:\n return "Goodbye!"', + source='async def run() -> str:\n return "Goodbye!"', description="Say goodbye", ) redis_store.save(another) @@ -353,7 +353,7 @@ def _make_skill_with_invalid_name(name: str) -> PythonSkill: # Create a valid skill first valid_skill = PythonSkill.from_source( name="temp_valid_name", - source="def run(): pass", + source="async def run(): pass", description="test", ) # Replace the name with the invalid one for testing @@ -431,7 +431,7 @@ def test_valid_skill_names_accepted(self, tmp_path: Path) -> None: for name in ["my_skill", "skill123", "_private", "CamelCase", "__dunder__"]: skill = PythonSkill.from_source( name=name, - source="def run(): pass", + source="async def run(): pass", description="test", ) store.save(skill) diff --git a/tests/test_skills.py b/tests/test_skills.py index 9252a7f..5ea4a00 100644 --- a/tests/test_skills.py +++ b/tests/test_skills.py @@ -52,7 +52,7 @@ def skill_file(self, tmp_path: Path) -> Path: A friendly greeting skill. """ - def run(target_name: str, enthusiasm: int = 1) -> str: + async def run(target_name: str, enthusiasm: int = 1) -> str: """Generate a greeting. Args: @@ -96,31 +96,34 @@ def test_has_source_property(self, skill_file: Path) -> None: skill = PythonSkill.from_file(skill_file) assert skill.source is not None - assert "def run(" in skill.source + assert "async def run(" in skill.source assert "Hello, {target_name}" in skill.source - def test_invoke_calls_function(self, skill_file: Path) -> None: + @pytest.mark.asyncio + async def test_invoke_calls_function(self, skill_file: Path) -> None: """Invoking skill calls the run() function.""" skill = PythonSkill.from_file(skill_file) - result = skill.invoke(target_name="Alice") + result = await skill.invoke(target_name="Alice") assert result == "Hello, Alice!" - def test_invoke_with_defaults(self, skill_file: Path) -> None: + @pytest.mark.asyncio + async def test_invoke_with_defaults(self, skill_file: Path) -> None: """Invoke uses default parameter values.""" skill = PythonSkill.from_file(skill_file) - result = skill.invoke(target_name="Bob", enthusiasm=3) + result = await skill.invoke(target_name="Bob", enthusiasm=3) assert result == "Hello, Bob!!!" - def test_invoke_validates_required_params(self, skill_file: Path) -> None: + @pytest.mark.asyncio + async def test_invoke_validates_required_params(self, skill_file: Path) -> None: """Invoke fails if required params missing.""" skill = PythonSkill.from_file(skill_file) with pytest.raises(TypeError): - skill.invoke() # Missing target_name + await skill.invoke() def test_skill_with_tools_access(self, tmp_path: Path) -> None: """Skill can reference tools in its code.""" @@ -129,7 +132,7 @@ def test_skill_with_tools_access(self, tmp_path: Path) -> None: dedent(''' """Scan a network target.""" - def run(target: str, tools) -> str: + async def run(target: str, tools) -> str: """Run a scan using tools. Args: @@ -157,7 +160,7 @@ def test_from_source_basic(self) -> None: source = dedent(''' """Add two numbers.""" - def run(a: int, b: int) -> int: + async def run(a: int, b: int) -> int: return a + b ''').strip() @@ -171,7 +174,7 @@ def test_from_source_with_description_override(self) -> None: """Description parameter overrides docstring.""" source = dedent(''' """Original description.""" - def run() -> str: + async def run() -> str: return "hello" ''').strip() @@ -186,7 +189,7 @@ def run() -> str: def test_from_source_validates_syntax(self) -> None: """Invalid syntax raises SyntaxError.""" with pytest.raises(SyntaxError): - PythonSkill.from_source(name="bad", source="def run( broken") + PythonSkill.from_source(name="bad", source="async def run( broken") def test_from_source_requires_run_function(self) -> None: """Must have run() function.""" @@ -201,20 +204,21 @@ def other_func(): def test_from_source_validates_name(self) -> None: """Name must be valid Python identifier.""" - source = "def run(): pass" + source = "async def run(): pass" with pytest.raises(ValueError, match="identifier"): PythonSkill.from_source(name="invalid-name", source=source) - def test_invoke_from_source_skill(self) -> None: + @pytest.mark.asyncio + async def test_invoke_from_source_skill(self) -> None: """Can invoke skill created from source.""" source = dedent(''' """Multiply numbers.""" - def run(x: int, y: int) -> int: + async def run(x: int, y: int) -> int: return x * y ''').strip() skill = PythonSkill.from_source(name="multiply", source=source) - result = skill.invoke(x=3, y=4) + result = await skill.invoke(x=3, y=4) assert result == 12 diff --git a/tests/test_skills_namespace_decoupling.py b/tests/test_skills_namespace_decoupling.py index 65c8e50..3f11967 100644 --- a/tests/test_skills_namespace_decoupling.py +++ b/tests/test_skills_namespace_decoupling.py @@ -24,7 +24,7 @@ def skill_library() -> SkillLibrary: # Add a test skill that accesses tools skill = PythonSkill.from_source( name="use_tools", - source='def run(val: str) -> str:\n return f"tools={tools}, val={val}"', + source='async def run(val: str) -> str:\n return f"tools={tools}, val={val}"', description="A skill that uses tools from namespace", ) library.add(skill) @@ -91,7 +91,7 @@ def test_invoke_uses_tools_from_namespace(self, skill_library: SkillLibrary) -> # Create a skill that returns what tools it sees skill = PythonSkill.from_source( name="echo_tools", - source="def run() -> str:\n return str(type(tools).__name__)", + source="async def run() -> str:\n return str(type(tools).__name__)", description="Returns tools type", ) skill_library.add(skill) @@ -114,7 +114,7 @@ def test_invoke_uses_artifacts_from_namespace(self, skill_library: SkillLibrary) """Skill invocation can access artifacts from namespace dict.""" skill = PythonSkill.from_source( name="use_artifacts", - source="def run() -> bool:\n return artifacts is not None", + source="async def run() -> bool:\n return artifacts is not None", description="Checks artifacts access", ) skill_library.add(skill) @@ -137,9 +137,11 @@ class TestNamespaceIsolation: def test_skill_cannot_modify_parent_namespace(self, skill_library: SkillLibrary) -> None: """Skill execution cannot add variables to parent namespace.""" - # Skill that tries to pollute namespace polluter_source = ( - 'def run() -> str:\n global pollution\n pollution = "leaked"\n return "done"' + "async def run() -> str:\n" + " global pollution\n" + ' pollution = "leaked"\n' + ' return "done"' ) skill = PythonSkill.from_source( name="polluter", @@ -162,9 +164,12 @@ def test_skill_cannot_modify_parent_namespace(self, skill_library: SkillLibrary) def test_skill_cannot_modify_tools_reference(self, skill_library: SkillLibrary) -> None: """Skill cannot replace tools in parent namespace.""" + replacer_source = ( + 'async def run() -> str:\n global tools\n tools = "replaced"\n return "done"' + ) skill = PythonSkill.from_source( name="replacer", - source='def run() -> str:\n global tools\n tools = "replaced"\n return "done"', + source=replacer_source, description="Tries to replace tools", ) skill_library.add(skill) diff --git a/tests/test_storage_vector_store.py b/tests/test_storage_vector_store.py index ba60874..6f46621 100644 --- a/tests/test_storage_vector_store.py +++ b/tests/test_storage_vector_store.py @@ -153,7 +153,7 @@ def test_skill_library_uses_vector_store_for_search(self, tmp_path: Path) -> Non # Add a skill with distinctive description skill = PythonSkill.from_source( name="calculate_total", - source="def run(numbers): return sum(numbers)", + source="async def run(numbers): return sum(numbers)", description="Add up all numbers in a list", ) library.add(skill) @@ -328,14 +328,14 @@ def test_file_storage_end_to_end_semantic_search(self, tmp_path: Path) -> None: library.add( PythonSkill.from_source( name="http_get", - source="def run(url): import requests; return requests.get(url)", + source="async def run(url): import requests; return requests.get(url)", description="Fetch data from a URL using HTTP GET request", ) ) library.add( PythonSkill.from_source( name="parse_json", - source="def run(text): import json; return json.loads(text)", + source="async def run(text): import json; return json.loads(text)", description="Parse JSON string into Python object", ) ) @@ -361,7 +361,7 @@ def test_vector_store_persists_across_storage_instances(self, tmp_path: Path) -> library1.add( PythonSkill.from_source( name="test_skill", - source="def run(): return 1", + source="async def run(): return 1", description="A test skill for persistence", ) ) @@ -417,7 +417,7 @@ def test_skill_library_works_without_vector_store(self, tmp_path: Path) -> None: # Should still work for basic operations skill = PythonSkill.from_source( name="basic", - source="def run(): return 1", + source="async def run(): return 1", description="Basic skill", ) library.add(skill) @@ -454,7 +454,7 @@ def test_vector_store_count_matches_skill_count(self, tmp_path: Path) -> None: library.add( PythonSkill.from_source( name=f"skill_{i}", - source="def run(): return 1", + source="async def run(): return 1", description=f"Skill number {i}", ) ) diff --git a/tests/test_store.py b/tests/test_store.py index a673f8b..3529915 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -73,7 +73,7 @@ def test_same_skill_same_hash(self) -> None: """Same skill content produces same hash.""" from py_code_mode.cli.store import _skill_hash - skill = _make_skill("test", "Test skill", "def run():\n return 'hello'") + skill = _make_skill("test", "Test skill", "async def run():\n return 'hello'") hash1 = _skill_hash(skill) hash2 = _skill_hash(skill) @@ -83,8 +83,8 @@ def test_different_content_different_hash(self) -> None: """Different skill content produces different hash.""" from py_code_mode.cli.store import _skill_hash - skill1 = _make_skill("test", "Desc 1", "def run(): return 1") - skill2 = _make_skill("test", "Desc 2", "def run(): return 1") + skill1 = _make_skill("test", "Desc 1", "async def run(): return 1") + skill2 = _make_skill("test", "Desc 2", "async def run(): return 1") assert _skill_hash(skill1) != _skill_hash(skill2) @@ -92,7 +92,7 @@ def test_hash_is_short(self) -> None: """Hash is truncated to 12 characters.""" from py_code_mode.cli.store import _skill_hash - skill = _make_skill("test", "desc", "def run(): pass") + skill = _make_skill("test", "desc", "async def run(): pass") assert len(_skill_hash(skill)) == 12 @@ -107,7 +107,7 @@ def test_bootstrap_loads_skills_from_directory(self, tmp_path: Path) -> None: skill_file = tmp_path / "my_skill.py" skill_file.write_text('''"""My test skill.""" -def run(x: int) -> int: +async def run(x: int) -> int: """Double a number.""" return x * 2 ''') @@ -129,11 +129,11 @@ def test_bootstrap_with_clear_removes_existing(self, tmp_path: Path) -> None: # Create test skill skill_file = tmp_path / "new_skill.py" - skill_file.write_text('"""New skill."""\ndef run() -> str:\n return "new"') + skill_file.write_text('"""New skill."""\nasync def run() -> str:\n return "new"') # Mock store with existing skill mock_store = MagicMock() - existing_skill = _make_skill("old_skill", "Old", "def run(): pass") + existing_skill = _make_skill("old_skill", "Old", "async def run(): pass") mock_store.list_all.return_value = [existing_skill] with patch("py_code_mode.cli.store._get_store", return_value=mock_store): @@ -147,8 +147,8 @@ def test_bootstrap_returns_count(self, tmp_path: Path) -> None: from py_code_mode.cli.store import bootstrap # Create multiple skills - (tmp_path / "skill1.py").write_text('"""S1."""\ndef run(): return 1') - (tmp_path / "skill2.py").write_text('"""S2."""\ndef run(): return 2') + (tmp_path / "skill1.py").write_text('"""S1."""\nasync def run(): return 1') + (tmp_path / "skill2.py").write_text('"""S2."""\nasync def run(): return 2') mock_store = MagicMock() mock_store.list_all.return_value = [] @@ -173,7 +173,7 @@ def test_pull_writes_skills_to_files(self, tmp_path: Path) -> None: skill = MagicMock() skill.name = "skill1" skill.description = "First skill" - skill.source = '"""First skill."""\ndef run():\n print("one")' + skill.source = '"""First skill."""\nasync def run():\n print("one")' mock_store.list_all.return_value = [skill] with patch("py_code_mode.cli.store._get_store", return_value=mock_store): @@ -215,7 +215,7 @@ def test_diff_finds_added_skills(self, tmp_path: Path) -> None: remote_skill = MagicMock() remote_skill.name = "agent_created" remote_skill.description = "Created by agent" - remote_skill.source = '"""Created by agent."""\ndef run(): pass' + remote_skill.source = '"""Created by agent."""\nasync def run(): pass' mock_store.list_all.return_value = [remote_skill] with patch("py_code_mode.cli.store._get_store", return_value=mock_store): @@ -232,7 +232,7 @@ def test_diff_finds_removed_skills(self, tmp_path: Path) -> None: # Local has a skill local = tmp_path / "local" local.mkdir() - (local / "local_only.py").write_text('"""Local skill."""\ndef run(): pass') + (local / "local_only.py").write_text('"""Local skill."""\nasync def run(): pass') # Remote is empty mock_store = MagicMock() @@ -248,17 +248,17 @@ def test_diff_finds_modified_skills(self, tmp_path: Path) -> None: """Diff identifies skills with different content.""" from py_code_mode.cli.store import diff - # Local skill local = tmp_path / "local" local.mkdir() - (local / "shared_skill.py").write_text('"""Local version."""\ndef run(): return "local"') + local_skill = '"""Local version."""\nasync def run(): return "local"' + (local / "shared_skill.py").write_text(local_skill) # Remote has different version mock_store = MagicMock() remote_skill = MagicMock() remote_skill.name = "shared_skill" remote_skill.description = "Remote version" - remote_skill.source = '"""Remote version."""\ndef run(): return "remote"' + remote_skill.source = '"""Remote version."""\nasync def run(): return "remote"' mock_store.list_all.return_value = [remote_skill] with patch("py_code_mode.cli.store._get_store", return_value=mock_store): @@ -273,7 +273,7 @@ def test_diff_finds_unchanged_skills(self, tmp_path: Path) -> None: # Local skill local = tmp_path / "local" local.mkdir() - skill_content = '"""Same skill."""\ndef run(): return "same"' + skill_content = '"""Same skill."""\nasync def run(): return "same"' (local / "same_skill.py").write_text(skill_content) # Remote has same content diff --git a/tests/test_subprocess_namespace_injection.py b/tests/test_subprocess_namespace_injection.py index e0e712b..e4c5ba4 100644 --- a/tests/test_subprocess_namespace_injection.py +++ b/tests/test_subprocess_namespace_injection.py @@ -129,7 +129,7 @@ async def test_tool_to_skill_to_artifact_workflow(self, executor_with_storage) - skills.create( name="greet", source=""" -def run(name: str) -> str: +async def run(name: str) -> str: result = tools.echo.run(text=f"Hello, {name}!") return result """, @@ -165,7 +165,7 @@ async def test_skill_uses_tools_namespace_internally(self, executor_with_storage skills.create( name="echo_wrapper", source=""" -def run(message: str) -> str: +async def run(message: str) -> str: return tools.echo.run(text=message) """, description="Wrapper around echo tool" @@ -262,7 +262,7 @@ async def test_skills_list_returns_skill_info(self, executor_empty_storage) -> N create_code = """ skills.create( name="add", - source="def run(a: int, b: int) -> int: return a + b", + source="async def run(a: int, b: int) -> int: return a + b", description="Add two numbers" ) """ @@ -284,7 +284,7 @@ async def test_skills_search_semantic(self, executor_empty_storage) -> None: create_code = """ skills.create( name="calculate_sum", - source="def run(numbers: list) -> int: return sum(numbers)", + source="async def run(numbers: list) -> int: return sum(numbers)", description="Calculate the total of a list of numbers" ) """ @@ -306,7 +306,7 @@ async def test_skills_create_persists(self, executor_empty_storage) -> None: create_code = """ skills.create( name="multiply", - source="def run(a: int, b: int) -> int: return a * b", + source="async def run(a: int, b: int) -> int: return a * b", description="Multiply two numbers" ) """ @@ -331,7 +331,7 @@ async def test_skills_invoke_executes_skill(self, executor_empty_storage) -> Non create_code = """ skills.create( name="square", - source="def run(n: int) -> int: return n * n", + source="async def run(n: int) -> int: return n * n", description="Square a number" ) """ @@ -353,7 +353,7 @@ async def test_skills_attribute_access_invocation(self, executor_empty_storage) create_code = """ skills.create( name="triple", - source="def run(n: int) -> int: return n * 3", + source="async def run(n: int) -> int: return n * 3", description="Triple a number" ) """ @@ -381,7 +381,7 @@ async def test_skills_invoke_uses_runtime_installed_dep(self, executor_empty_sto skills.create( name="ascii_art_test", source=''' -def run(text: str) -> str: +async def run(text: str) -> str: import art return art.text2art(text, font="block") ''', @@ -541,7 +541,7 @@ async def test_namespace_state_persists_between_runs(self, executor_empty_storag create_code = """ skills.create( name="counter", - source="def run(): return 'counted'", + source="async def run(): return 'counted'", description="Simple counter" ) """ @@ -578,9 +578,11 @@ async def test_namespace_state_preserved_after_reset(self, tmp_path: Path) -> No try: await executor.start(storage=storage) - # Create skill and artifact before reset await executor.run( - 'skills.create(name="persist", source="def run(): return 1", description="test")' + "skills.create(" + 'name="persist", ' + 'source="async def run(): return 1", ' + 'description="test")' ) await executor.run('artifacts.save("persist_artifact", "value")') @@ -714,7 +716,7 @@ async def test_creating_skill_with_invalid_source_raises_error( result = await executor_empty_storage.run( """skills.create( name="broken", - source="def run( this is not valid python {{{{", + source="async def run( this is not valid python {{{{", description="Broken skill" )""" ) diff --git a/tests/test_subprocess_rpc.py b/tests/test_subprocess_rpc.py index 764ba3b..647189d 100644 --- a/tests/test_subprocess_rpc.py +++ b/tests/test_subprocess_rpc.py @@ -237,9 +237,9 @@ def test_get_kernel_init_code_accepts_timeout(self) -> None: assert "_RPC_TIMEOUT = 60.0" in code def test_get_kernel_init_code_default_timeout(self) -> None: - """get_kernel_init_code uses default timeout of 30.0.""" + """get_kernel_init_code uses unlimited timeout by default.""" code = get_kernel_init_code() - assert "_RPC_TIMEOUT = 30.0" in code + assert "_RPC_TIMEOUT = None" in code def test_kernel_init_code_defines_rpc_call(self) -> None: """KERNEL_INIT_CODE defines _rpc_call function.""" diff --git a/tests/test_subprocess_vector_store.py b/tests/test_subprocess_vector_store.py index fdebad2..11da20f 100644 --- a/tests/test_subprocess_vector_store.py +++ b/tests/test_subprocess_vector_store.py @@ -241,7 +241,7 @@ async def test_subprocess_executor_with_vector_store(self, tmp_path: Path) -> No library.add( PythonSkill.from_source( name="fetch_url", - source="def run(url): import requests; return requests.get(url).text", + source="async def run(url): import requests; return requests.get(url).text", description="Download content from a web URL using HTTP", ) ) @@ -297,7 +297,7 @@ async def test_subprocess_executor_without_vector_store_falls_back( create_code = """ skills.create( name="test_skill", - source="def run(): return 1", + source="async def run(): return 1", description="Test skill for fallback" ) """ diff --git a/tests/test_vector_store.py b/tests/test_vector_store.py index 9fec444..ab340d7 100644 --- a/tests/test_vector_store.py +++ b/tests/test_vector_store.py @@ -189,7 +189,7 @@ def test_compute_content_hash_exists(self) -> None: hash_value = compute_content_hash( description="Scan network ports", - source='def run(): return "nmap"', + source='async def run(): return "nmap"', ) # Should return a string (hex digest) @@ -199,7 +199,7 @@ def test_content_hash_is_16_chars(self) -> None: """Hash should be 16-character hex string (8 bytes).""" from py_code_mode.skills.vector_store import compute_content_hash - hash_value = compute_content_hash(description="test", source="def run(): pass") + hash_value = compute_content_hash(description="test", source="async def run(): pass") assert len(hash_value) == 16 # Should be valid hex @@ -210,7 +210,7 @@ def test_same_input_produces_same_hash(self) -> None: from py_code_mode.skills.vector_store import compute_content_hash description = "Scan network ports" - source = 'def run(target: str):\n return f"nmap {target}"' + source = 'async def run(target: str):\n return f"nmap {target}"' hash1 = compute_content_hash(description, source) hash2 = compute_content_hash(description, source) @@ -221,7 +221,7 @@ def test_different_description_produces_different_hash(self) -> None: """Different description should change hash.""" from py_code_mode.skills.vector_store import compute_content_hash - source = "def run(): pass" + source = "async def run(): pass" hash1 = compute_content_hash("Description A", source) hash2 = compute_content_hash("Description B", source) @@ -234,8 +234,8 @@ def test_different_source_produces_different_hash(self) -> None: description = "Test skill" - hash1 = compute_content_hash(description, "def run(): return 1") - hash2 = compute_content_hash(description, "def run(): return 2") + hash1 = compute_content_hash(description, "async def run(): return 1") + hash2 = compute_content_hash(description, "async def run(): return 2") assert hash1 != hash2 @@ -244,8 +244,8 @@ def test_whitespace_changes_affect_hash(self) -> None: from py_code_mode.skills.vector_store import compute_content_hash description = "Test" - source1 = "def run(): pass" - source2 = "def run(): pass" # Extra space + source1 = "async def run(): pass" + source2 = "async def run(): pass" # Extra space hash1 = compute_content_hash(description, source1) hash2 = compute_content_hash(description, source2) @@ -257,7 +257,7 @@ def test_hash_uses_sha256_algorithm(self) -> None: from py_code_mode.skills.vector_store import compute_content_hash description = "Test description" - source = "def run(): pass" + source = "async def run(): pass" # Compute what the hash SHOULD be combined = f"{description}|||{source}" @@ -331,7 +331,7 @@ def count(self) -> int: store.add( id="test_skill", description="Test description", - source="def run(): pass", + source="async def run(): pass", content_hash="abcd1234", ) diff --git a/uv.lock b/uv.lock index b477bf3..39dabc2 100644 --- a/uv.lock +++ b/uv.lock @@ -455,6 +455,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/36/7d2d7b6bb26e53214492d71ccb4e128fa2de4d98a215befb7787deaf2701/chromadb-1.3.7-cp39-abi3-win_amd64.whl", hash = "sha256:4618ba7bb5ef5dbf0d4fd9ce708b912d8cd1ab24d3c81e0e092841f325b2c94d", size = 21874973, upload-time = "2025-12-12T21:03:16.918Z" }, ] +[[package]] +name = "claude-agent-sdk" +version = "0.1.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "mcp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/3d/a8c6ad873e8448696d44441c9eb2c24dded620fb32415d68f576a542ccde/claude_agent_sdk-0.1.18.tar.gz", hash = "sha256:4fcb8730cc77dea562fbe9aa48c65eced3ef58a6bb1f34f77e50e8258902477d", size = 56162, upload-time = "2025-12-18T00:42:57.926Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/14/f529f7c4bab7c71dcbcc8c66f12f491e644ee8a027ac5111d13705df207e/claude_agent_sdk-0.1.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9e45b4e3c20c072c3e3325fa60bab9a4b5a7cbbce64ca274b8d7d0af42dd9dd8", size = 54560828, upload-time = "2025-12-18T00:42:44.71Z" }, + { url = "https://files.pythonhosted.org/packages/2c/68/6e83005aa7bb9056bfad0aef0605249f877dc0c78724c9c0fadebff600fb/claude_agent_sdk-0.1.18-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:3c41bd8f38848609ae0d5da8d7327a4c2d7057a363feafb6fd70df611ea204cc", size = 68743107, upload-time = "2025-12-18T00:42:48.255Z" }, + { url = "https://files.pythonhosted.org/packages/fb/85/7d6dd85f402135a610894734c442f1166ffed61d03eced39d6bfd14efccd/claude_agent_sdk-0.1.18-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:983f15e51253f40c55136a86d7cc63e023a3576428b05fa1459093d461b2d215", size = 70444964, upload-time = "2025-12-18T00:42:51.752Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fa/d2b22b7a713c4c049cbd5f9f635836ea5429ff65c1f3bcf4658a8e1c1cf5/claude_agent_sdk-0.1.18-py3-none-win_amd64.whl", hash = "sha256:36f5b84d5c3c8773ee9b56aeb5ab345d1033231db37f80d1f20ac15239bef41c", size = 72637215, upload-time = "2025-12-18T00:42:55.269Z" }, +] + [[package]] name = "click" version = "8.3.1" @@ -2581,6 +2597,7 @@ name = "py-code-mode" version = "0.9.0" source = { editable = "." } dependencies = [ + { name = "claude-agent-sdk" }, { name = "fastmcp" }, { name = "filelock" }, { name = "jupyter-client" }, @@ -2641,6 +2658,7 @@ dev = [ requires-dist = [ { name = "aiohttp", marker = "extra == 'http'", specifier = ">=3.9" }, { name = "chromadb", marker = "extra == 'chromadb'", specifier = ">=0.5" }, + { name = "claude-agent-sdk", specifier = ">=0.1.18" }, { name = "docker", marker = "extra == 'container'", specifier = ">=7.0" }, { name = "fastapi", marker = "extra == 'container'", specifier = ">=0.100" }, { name = "fastmcp", specifier = ">=2.0.0" },