Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
- name: Install dependencies
run: |
# create venv in repo so it can be cached
python -m venv .venv
python -m venv --clear .venv
source .venv/bin/activate
pip install --upgrade pip
uv sync --dev
Expand Down
86 changes: 86 additions & 0 deletions agentflow_cli/cli/commands/skill.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Skill command — generate coding-agent skill files (CLAUDE.md, .cursorrules, ...)."""

from pathlib import Path
from typing import Any

from agentflow_cli.cli.commands import BaseCommand
from agentflow_cli.cli.exceptions import FileOperationError
from agentflow_cli.cli.templates.skills import SKILL_TARGETS


class SkillCommand(BaseCommand):
"""Write AgentFlow skill files for common coding agents."""

def execute(
self,
path: str = ".",
agent: str = "all",
force: bool = False,
**kwargs: Any,
) -> int:
"""Generate skill files.

Args:
path: Project directory to generate into.
agent: Agent id to target. One of the keys in ``SKILL_TARGETS`` or ``"all"``.
force: Overwrite existing files.
"""
try:
subtitle = (
"Generate AgentFlow skill files for coding agents "
"(Claude Code, Cursor, Copilot, Windsurf, Codex)"
)
self.output.print_banner("Skill", subtitle, color="cyan")

base_path = Path(path)
base_path.mkdir(parents=True, exist_ok=True)

agent_key = agent.lower().strip()
if agent_key == "all":
targets = SKILL_TARGETS
elif agent_key in SKILL_TARGETS:
targets = {agent_key: SKILL_TARGETS[agent_key]}
else:
supported = ", ".join(sorted(["all", *SKILL_TARGETS]))
raise FileOperationError(f"Unknown agent '{agent}'. Supported: {supported}.")

written: list[Path] = []
for name, (rel_path, content) in targets.items():
dest = base_path / rel_path
self._write_file(dest, content, force=force)
written.append(dest)
self.output.success(f"[{name}] wrote {dest}")

self.output.info("\n🚀 Next steps:")
steps = [
"Commit the generated files so your coding agent picks them up.",
"Edit the content to capture any project-specific conventions.",
"Re-run 'agentflow skill --force' whenever AgentFlow usage changes.",
]
for i, step in enumerate(steps, 1):
self.output.info(f"{i}. {step}")

return 0

except FileOperationError as e:
return self.handle_error(e)
except Exception as e:
return self.handle_error(FileOperationError(f"Failed to generate skill files: {e}"))

def _write_file(self, path: Path, content: str, *, force: bool) -> None:
try:
path.parent.mkdir(parents=True, exist_ok=True)

if path.exists() and not force:
raise FileOperationError(
f"File already exists: {path}. Use --force to overwrite.",
file_path=str(path),
)

path.write_text(content, encoding="utf-8")
self.logger.debug("Wrote skill file: %s", path)

except OSError as e:
raise FileOperationError(
f"Failed to write file {path}: {e}", file_path=str(path)
) from e
48 changes: 48 additions & 0 deletions agentflow_cli/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from agentflow_cli.cli.commands.api import APICommand
from agentflow_cli.cli.commands.build import BuildCommand
from agentflow_cli.cli.commands.init import InitCommand
from agentflow_cli.cli.commands.skill import SkillCommand
from agentflow_cli.cli.commands.version import VersionCommand
from agentflow_cli.cli.constants import (
DEFAULT_CONFIG_FILE,
Expand Down Expand Up @@ -308,6 +309,53 @@ def build(
sys.exit(handle_exception(e))


@app.command()
def skill(
path: str = typer.Option(
".",
"--path",
"-p",
help="Directory to generate skill files in",
),
agent: str = typer.Option(
"all",
"--agent",
"-a",
help=(
"Coding agent to target: 'claude', 'cursor', 'copilot', 'windsurf', "
"'codex', or 'all' (default)"
),
),
force: bool = typer.Option(
False,
"--force",
"-f",
help="Overwrite existing skill files if they exist",
),
verbose: bool = typer.Option(
False,
"--verbose",
"-v",
help="Enable verbose logging",
),
quiet: bool = typer.Option(
False,
"--quiet",
"-q",
help="Suppress all output except errors",
),
) -> None:
"""Generate AgentFlow skill files for coding agents (Claude Code, Cursor, Copilot, ...)."""
setup_cli_logging(verbose=verbose, quiet=quiet)

try:
command = SkillCommand(output)
exit_code = command.execute(path=path, agent=agent, force=force)
sys.exit(exit_code)
except Exception as e:
sys.exit(handle_exception(e))


def main() -> None:
"""Main CLI entry point."""
try:
Expand Down
158 changes: 158 additions & 0 deletions agentflow_cli/cli/templates/agentflow_skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# AgentFlow

This project uses **AgentFlow** (`10xscale-agentflow`) — a graph-based Python
framework for building LLM agents. Use this guide when writing or modifying
agent code in this repo.

## When to use AgentFlow primitives

- **`StateGraph`** — the top-level container. Nodes run, edges decide what runs next.
- **`Agent`** — a node that calls an LLM. Handles provider SDKs, tool routing, memory, retries.
- **`ToolNode`** — a node that executes Python functions when the LLM requests tool calls.
- **`AgentState`** / **`Message`** — conversation state and messages flowing through the graph.
- **`InMemoryCheckpointer`** / other checkpointers — persist state per `thread_id`.

Don't reach for raw provider SDKs (`openai`, `google-genai`) inside a graph node.
Use `Agent` with the right `provider=` instead — it handles message conversion,
streaming, tool calls, retries, and fallbacks uniformly.

## Golden-path ReAct agent

```python
from dotenv import load_dotenv

from agentflow.core import Agent, StateGraph, ToolNode
from agentflow.core.state import AgentState, Message
from agentflow.storage.checkpointer import InMemoryCheckpointer
from agentflow.utils.constants import END

load_dotenv()


def get_weather(location: str) -> str:
"""Get the current weather for a location."""
return f"The weather in {location} is sunny"


tool_node = ToolNode([get_weather])

agent = Agent(
model="gpt-4o",
provider="openai",
system_prompt=[{"role": "system", "content": "You are a helpful assistant."}],
tool_node=tool_node,
)


def should_use_tools(state: AgentState) -> str:
last = state.context[-1] if state.context else None
if last and getattr(last, "tools_calls", None) and last.role == "assistant":
return "TOOL"
if last and last.role == "tool":
return "MAIN"
return END


graph = StateGraph()
graph.add_node("MAIN", agent)
graph.add_node("TOOL", tool_node)
graph.add_conditional_edges("MAIN", should_use_tools, {"TOOL": "TOOL", END: END})
graph.add_edge("TOOL", "MAIN")
graph.set_entry_point("MAIN")

app = graph.compile(checkpointer=InMemoryCheckpointer())

# Invoke with a stable thread_id to get persistent history
res = app.invoke(
{"messages": [Message.text_message("What is the weather in NYC?")]},
config={"thread_id": "demo", "recursion_limit": 10},
)
```

## Providers

Pick a provider via the `provider=` argument on `Agent`. Model names are free-form strings.

| Provider | Auth | Models |
|---|---|---|
| `"openai"` | `OPENAI_API_KEY` | `gpt-4o`, `gpt-4o-mini`, `o1`, `o3`, `o4-mini` |
| `"google"` | `GEMINI_API_KEY` / `GOOGLE_API_KEY` | `gemini-2.0-flash`, `gemini-2.5-flash`, `gemini-2.5-pro` |
| `"vertex_ai"` | `GOOGLE_CLOUD_PROJECT` + Application Default Credentials | same Gemini models |

Rules:

- If `provider` is omitted, it is inferred: `gpt*|o1|o3|o4` → `openai`, `gemini*` → `google`.
- `vertex_ai` is **never** inferred — set it explicitly.
- `google` and `vertex_ai` share model names and features; only auth differs.
- `google-genai` is not bundled. Install it when using either Gemini path:
`pip install google-genai`.

## Agent parameters worth knowing

```python
Agent(
model="gpt-4o",
provider="openai",
system_prompt=[{"role": "system", "content": "..."}],
tool_node=tool_node, # ToolNode instance or str name
output_type="text", # "text" | "json"
trim_context=True, # trim to model context window
reasoning_config=True, # enable extended thinking (o1/o3/gemini-2.5)
retry_config=True, # retry transient API errors
fallback_models=[ # try these if primary fails
"gpt-4o-mini",
("gemini-2.5-flash", "google"),
],
memory=my_memory_config, # long-term memory retrieval
skills=my_skill_config, # inject skill documents
multimodal_config=my_mm_config, # auto-offload large inline media
)
```

## State & messages

- Custom state extends `AgentState`:

```python
class MyState(AgentState):
user_id: str | None = None
```

- `Message.text_message("...")` builds a user message quickly.
- Tool functions can accept `tool_call_id: str` and `state: MyState` — they are
injected automatically if declared.

## Checkpointing

- `InMemoryCheckpointer()` for dev / tests.
- `PgCheckpointer` / `RedisCheckpointer` for production
(requires `REDIS_URL` or a Postgres DSN).
- Always pass `config={"thread_id": "..."}` on `invoke` / `stream` to keep
history across calls.

## CLI

AgentFlow ships a CLI (`10xscale-agentflow-cli`):

- `agentflow init` — scaffold `agentflow.json` and `graph/react.py`.
- `agentflow api` — run the FastAPI server over your graph.
- `agentflow play` — start the API + open the hosted playground.
- `agentflow build` — generate a `Dockerfile` (optionally `docker-compose.yml`).
- `agentflow skill` — regenerate these coding-agent skill files.

`agentflow.json` points at the compiled graph (`"agent": "graph.react:app"`).

## Don't

- Don't call provider SDKs directly from a graph node. Use `Agent`.
- Don't mutate `state` in place — return a new state/messages from your node.
- Don't infer `vertex_ai` from the model name. Pass it explicitly.
- Don't commit API keys; keep them in `.env` (referenced by `agentflow.json`).
- Don't hand-roll retry loops around `Agent` — use `retry_config=True`.

## Where to look

- Python reference: `agentflow.core`, `agentflow.core.graph`, `agentflow.core.state`,
`agentflow.storage.checkpointer`, `agentflow.utils.constants`.
- Runtime adapters (`GoogleGenAIConverter`, OpenAI converters) live under
`agentflow.runtime.adapters.llm` — only needed when wrapping raw SDK calls.
32 changes: 32 additions & 0 deletions agentflow_cli/cli/templates/skills.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Skill templates for coding agents (Claude Code, Cursor, Copilot, Windsurf, Codex).

The shared body lives in ``agentflow_skill.md`` so the rendered markdown can
remain well-formatted without fighting Python line-length rules.
"""

from __future__ import annotations

from pathlib import Path
from typing import Final


_TEMPLATE_DIR: Final[Path] = Path(__file__).parent
_SKILL_BODY: Final[str] = (_TEMPLATE_DIR / "agentflow_skill.md").read_text(encoding="utf-8")


_CURSOR_FRONTMATTER: Final[str] = (
"---\n"
"description: AgentFlow framework conventions and golden-path snippets\n"
"alwaysApply: true\n"
"---\n\n"
)


# Registry mapping agent id -> (relative output path, file content).
SKILL_TARGETS: Final[dict[str, tuple[str, str]]] = {
"claude": ("CLAUDE.md", _SKILL_BODY),
"cursor": (".cursor/rules/agentflow.mdc", _CURSOR_FRONTMATTER + _SKILL_BODY),
"copilot": (".github/copilot-instructions.md", _SKILL_BODY),
"windsurf": (".windsurfrules", _SKILL_BODY),
"codex": ("AGENTS.md", _SKILL_BODY),
}
Loading