--- name: ak-build description: > Add tools, agents, and handoffs to an existing Agent Kernel project. This skill guides you through reading the current project, understanding its framework and structure, then making context-aware additions — new tools, new agents, agent-to-agent handoffs, and Module wiring. The workhorse skill for iterative agent development. license: Apache-2.0 metadata: author: yaalalabs category: user --- # Build: Add Tools & Agents Use this skill to add new tools, agents, or handoffs to an existing Agent Kernel project. ## Instructions for the Agent ### Step 1: Read the Existing Project **Before generating any code**, inspect the project to determine: 1. **Framework** — Open the main agent file (e.g., `app.py`, `demo.py`, `server.py`, `lambda.py`) and look for: - `from agentkernel.openai import OpenAIModule` → **OpenAI Agents SDK** - `from agentkernel.langgraph import LangGraphModule` → **LangGraph** - `from agentkernel.crewai import CrewAIModule` → **CrewAI** - `from agentkernel.adk import GoogleADKModule` → **Google ADK** - `from agentkernel.smolagents import SmolagentsModule` → **Smolagents** - `from agentkernel.pydanticai import PydanticAIModule` → **Pydantic AI** 2. **Existing agents** — List every agent already defined (names, roles, instructions). 3. **Existing tools** — List every tool function and which agent uses it. 4. **Entry point** — Is it CLI (`demo.py`), API (`RESTAPI.run()`), Lambda (`Lambda.handler`), or Azure Function? 5. **Config** — Read `config.yaml` for session type, guardrails, tracing, integrations already enabled. 6. **Dependencies** — Read `pyproject.toml` for the extras already installed (e.g., `[openai,api,redis]`). Report back what you found before proceeding. Example: > **Project summary:** > - Framework: OpenAI Agents SDK > - Entry point: `app.py` (API mode via `RESTAPI.run()`) > - Agents: `triage` (routes to sub-agents), `math` (handles math), `general` (handles everything else) > - Tools: `calculator` (bound to `math`), `web_search` (bound to `general`) > - Session: Redis > - Extras: `[openai,api,redis]` --- ### Step 2: Ask What to Add Ask the user what they want to add: 1. **A new tool** — A Python function that an agent can call 2. **A new agent** — A new specialist agent 3. **A handoff** — Wire an existing agent to delegate to another 4. **All of the above** — Add a new agent with its own tools and wire it into the existing handoff graph --- ### Step 3: Add a Tool #### 3a. Write the Tool Function Create the tool function in the project's tool file (usually `tool.py`, or wherever existing tools live). **Rules:** - Tool functions are **plain Python functions** (sync or async) with type annotations and a docstring. - Use `ToolContext.get()` **inside** the function body to access session and runtime. Never pass context as a parameter. - Use `__` (double underscore) as the nested delimiter in environment variable names (e.g., `AK_REDIS__URL`). ```python from agentkernel.core import ToolContext def lookup_order(order_id: str) -> str: """Look up an order by its ID and return the order details.""" context = ToolContext.get() session = context.session # Use session cache for expensive lookups cache = session.get_non_volatile_cache() cached = cache.get(f"order:{order_id}") if cached: return cached # Your lookup logic here result = f"Order {order_id}: shipped, arriving tomorrow" cache[f"order:{order_id}"] = result return result ``` #### 3b. Bind the Tool to an Agent The binding syntax depends on the framework. Match what the project already uses: **OpenAI Agents SDK:** ```python from agentkernel.openai import OpenAIToolBuilder tools = OpenAIToolBuilder.bind([lookup_order, existing_tool_1]) agent = Agent(name="support", instructions="...", tools=tools) ``` **LangGraph:** ```python from agentkernel.langgraph import LangGraphToolBuilder tools = LangGraphToolBuilder.bind([lookup_order, existing_tool_1]) agent = create_react_agent(name="support", tools=tools, model=model, prompt="...") ``` **CrewAI:** ```python from agentkernel.crewai import CrewAIToolBuilder tools = CrewAIToolBuilder.bind([lookup_order, existing_tool_1]) agent = Agent(role="support", goal="...", backstory="...", tools=tools, verbose=False) ``` **Google ADK:** ```python from agentkernel.adk import GoogleADKToolBuilder tools = GoogleADKToolBuilder.bind([lookup_order, existing_tool_1]) agent = Agent(name="support", model=LiteLlm(model="openai/gpt-4o-mini"), description="...", instruction="...", tools=tools) ``` **Smolagents:** ```python from agentkernel.smolagents import SmolagentsToolBuilder tools = SmolagentsToolBuilder.bind([lookup_order, existing_tool_1]) agent = ToolCallingAgent( tools=tools, model=model, name="support", description="...", ) ``` **Pydantic AI:** ```python from agentkernel.pydanticai import PydanticAIToolBuilder tools = PydanticAIToolBuilder.bind([lookup_order, existing_tool_1]) agent = Agent(model="openai:gpt-4o-mini", name="support", description="...", instructions="...", tools=tools) ``` > **Gotcha:** Always add the new tool to the **existing** `bind()` call for that agent. Don't create a second `bind()`. --- ### Step 4: Add an Agent #### 4a. Define the Agent Match the framework already in use: **OpenAI Agents SDK:** ```python from agents import Agent from agentkernel.openai import OpenAIToolBuilder support_agent = Agent( name="support", handoff_description="Specialist for customer support and order inquiries", instructions="You help customers with order lookups, returns, and general support questions.", tools=OpenAIToolBuilder.bind([lookup_order]), ) ``` **LangGraph:** ```python from langchain.chat_models import init_chat_model from langgraph.prebuilt import create_react_agent from agentkernel.langgraph import LangGraphToolBuilder model = init_chat_model("openai:gpt-4o-mini") support_agent = create_react_agent( name="support", # Always pass name= explicitly! tools=LangGraphToolBuilder.bind([lookup_order]), model=model, prompt="You help customers with order lookups, returns, and general support questions.", ) ``` > **Gotcha (LangGraph):** Always pass `name=` to `create_react_agent()`. Without it the agent is unnamed and the supervisor cannot route to it. **CrewAI:** ```python from crewai import Agent from agentkernel.crewai import CrewAIToolBuilder support_agent = Agent( role="support", # CrewAI uses role= as the identifier, NOT name= goal="Specialist for customer support and order inquiries", backstory="You help customers with order lookups, returns, and general support questions.", tools=CrewAIToolBuilder.bind([lookup_order]), verbose=False, ) ``` > **Gotcha (CrewAI):** Use `role=` as the agent identifier — Agent Kernel maps `agent.role` to the agent name. Setting `verbose=False` keeps output clean. **Google ADK:** ```python from google.adk.agents import Agent from google.adk.models.lite_llm import LiteLlm from agentkernel.adk import GoogleADKToolBuilder support_agent = Agent( name="support", model=LiteLlm(model="openai/gpt-4o-mini"), # Always wrap in LiteLlm() description="Specialist for customer support and order inquiries", instruction="You help customers with order lookups, returns, and general support questions.", tools=GoogleADKToolBuilder.bind([lookup_order]), ) ``` > **Gotcha (Google ADK):** Use `LiteLlm(model="openai/gpt-4o-mini")` — never pass a bare model string like `"gpt-4o-mini"`. **Smolagents:** ```python from smolagents import LiteLLMModel, ToolCallingAgent from agentkernel.smolagents import SmolagentsToolBuilder model = LiteLLMModel(model_id="openai/gpt-4o") support_agent = ToolCallingAgent( tools=SmolagentsToolBuilder.bind([lookup_order]), model=model, name="support", description="You help customers with order lookups, returns, and general support questions.", ) ``` **Pydantic AI:** ```python from pydantic_ai import Agent from agentkernel.pydanticai import PydanticAIToolBuilder support_agent = Agent( model="openai:gpt-4o-mini", # provider-agnostic — swap for "anthropic:...", "google-gla:...", etc. name="support", # Always pass name= explicitly! description="Specialist for customer support and order inquiries", instructions="You help customers with order lookups, returns, and general support questions.", tools=PydanticAIToolBuilder.bind([lookup_order]), ) ``` > **Gotcha (Pydantic AI):** Always pass `name=` (AK registers by name eagerly; Pydantic AI otherwise infers it lazily at first run) **and** `description=` (Pydantic AI's description is optional but is what AK reports as the agent description / A2A summary). The provider key for the model string (e.g. `OPENAI_API_KEY`) must be set at import time — Pydantic AI resolves the provider at construction. #### 4b. Register with the Module Add the new agent to the **existing** Module constructor call. Do not create a second Module. ```python # Before: OpenAIModule([triage_agent, math_agent, general_agent]) # After: OpenAIModule([triage_agent, math_agent, general_agent, support_agent]) ``` This applies to all frameworks — `LangGraphModule`, `CrewAIModule`, `GoogleADKModule`, `SmolagentsModule`, `PydanticAIModule` work the same way. #### 4c. Structured Output (Optional) To make an agent return a typed dict instead of plain text, define a Pydantic model and configure it on the agent (or, for CrewAI, on the module). The runner returns an `AgentReplyAny` whose `content` is the result as a dict; `str(reply)` is the JSON serialization, so text-based consumers (CLI, chat integrations) work unchanged. Applies to non-streaming execution only. ```python from pydantic import BaseModel class OrderStatus(BaseModel): order_id: str status: str ``` | Framework | How to configure | |-----------|------------------| | OpenAI Agents SDK | `Agent(..., output_type=OrderStatus)` | | LangGraph | `create_react_agent(..., response_format=OrderStatus)` | | CrewAI | `CrewAIModule([agent], output_pydantic={"support": OrderStatus})` — or `output_json={...}`; keyed by agent `role` | | Google ADK | `LlmAgent(..., output_schema=OrderStatus)` | | Smolagents | No schema parameter — have the agent pass a dict or Pydantic instance to `final_answer` | > **Gotcha (CrewAI):** CrewAI puts the output schema on the `Task`, not the `Agent` — and Agent Kernel builds the task internally per run, so the schema is passed to the `CrewAIModule` constructor keyed by agent role. --- ### Step 5: Add Handoffs Wire the new agent into the existing routing so the triage/supervisor agent can delegate to it. **OpenAI Agents SDK:** Add the new agent to the triage agent's `handoffs` list: ```python triage_agent = Agent( name="triage", instructions="You route requests to the right specialist agent...", handoffs=[math_agent, general_agent, support_agent], # Add here ) ``` Update the triage instructions to mention the new agent: ```python instructions = """You route user requests to the right specialist: - math agent: for calculations and math problems - general agent: for general knowledge questions - support agent: for customer support and order inquiries ← ADD THIS """ ``` **LangGraph:** Add the new agent to the supervisor's `agents` list: ```python from langgraph_supervisor import create_supervisor triage_agent = create_supervisor( model=model, agents=[math_agent, general_agent, support_agent], # Add here prompt="Route requests to the right specialist...", ).compile(name="triage") ``` **CrewAI:** No explicit routing — CrewAI automatically makes all agents in the Module available. Just add the new agent to the Module list (Step 4b). The `Crew` will include it. **Google ADK:** Add the new agent to the triage agent's `sub_agents`: ```python from google.adk.agents import LlmAgent triage_agent = LlmAgent( name="triage", model=LiteLlm(model="openai/gpt-4o-mini"), description="Routes requests to specialists", instruction="""Route requests to the right specialist. Use transfer_to_agent to delegate: - math: for calculations - general: for general knowledge - support: for customer support and order inquiries ← ADD THIS """, sub_agents=[math_agent, general_agent, support_agent], # Add here ) ``` **Smolagents:** Add the new agent to the triage agent's `managed_agents` list: ```python triage_agent = ToolCallingAgent( tools=[], model=model, name="triage", description="You determine which agent to use based on the user's question.", managed_agents=[math_agent, general_agent, support_agent], # Add here ) ``` --- ### Step 6: Add Hooks (Optional) Attach pre/post processing to the new agent. See the `ak-add-capabilities` skill for full hook patterns. ```python # Pre-hook: runs before the agent processes the request module.pre_hook(support_agent, [RAGPreHook()]) # Post-hook: runs after the agent generates a response module.post_hook(support_agent, [DisclaimerPostHook()]) ``` Where `module` is the framework Module instance (e.g., `OpenAIModule`). To use hooks, assign the Module to a variable: ```python module = OpenAIModule([triage_agent, math_agent, general_agent, support_agent]) module.pre_hook(support_agent, [RAGPreHook()]) ``` **Framework-native run options (optional):** to pass the framework's own run arguments for one agent (OpenAI `max_turns`, `hooks=RunHooks()` and `run_config=RunConfig(...)`; LangGraph `config`; ADK `plugins` and `run_config`; Pydantic AI `usage_limits`; CrewAI `step_callback`; smolagents `max_steps`), declare them with `run_options` on the same module, in the framework's own types: ```python from agents import RunConfig module.run_options(support_agent, max_turns=25, hooks=ProgressHooks(), run_config=RunConfig(...)) ``` Keys the adapter populates itself (`session`, `context`, `input`, ...) raise `ValueError` at declaration. See the `ak-add-capabilities` skill for the per-framework destinations. To compute options per run, give `run_options` a callable before the keywords; it is called with `(agent, session, requests)` on every run and its mapping is merged over the static keywords: ```python def options_for(agent, session, requests): return {"max_turns": 10} if session.id.startswith("guest") else {} module.run_options(support_agent, options_for, max_turns=25, hooks=ProgressHooks()) ``` --- ### Step 7: Update Dependencies (If Needed) If the new tool or agent requires additional packages, update `pyproject.toml`: ```toml dependencies = [ "agentkernel[openai,api,redis]>=0.9.5", "httpx>=0.27.0", # Add any new deps for your tool ] ``` Then run: ```bash uv sync ``` --- ### Step 8: Verify 1. **Syntax check** — Run: `uv run python -c "import app"` (or whatever the entry file is) 2. **Test the new agent** — Add a test for the new agent (see `ak-test` skill for test setup): ```python @pytest.mark.order(10) async def test_support_routing(test_client): await test_client.send("What's the status of order 12345?") await test_client.expect(["order", "12345", "shipped"]) ``` 3. **Run tests**: ```bash uv run pytest -v ``` 4. **Manual test** (API mode): ```bash python app.py & curl -X POST http://localhost:8000/run \ -H "Content-Type: application/json" \ -d '{"prompt": "Check order 12345", "session_id": "test-1", "agent": "triage"}' ``` --- ### Common Gotchas | Gotcha | Details | |--------|---------| | **ToolContext access** | Always use `ToolContext.get()` inside the tool function body. Never pass context as a function parameter. | | **LangGraph `name=`** | Always pass `name=` to `create_react_agent()`. Without it, the supervisor cannot route to the agent. | | **CrewAI `role=`** | Use `role=` as the agent identifier, not `name=`. Agent Kernel reads `agent.role` as the agent name. | | **CrewAI `verbose=`** | Set `verbose=False` on agents to prevent noisy console output. | | **CrewAI conversation history** | CrewAI runner keeps its own per-session transcript (last 20 lines) prepended to each task description, independent of the Memory feature. If `Memory.remember()` fails (e.g. no embedder configured), the runner logs a warning and continues instead of failing the run. | | **CrewAI structured output** | Configured on the module, not the agent: `CrewAIModule([agent], output_pydantic={"": Model})`. The native `crewai.Agent` rejects an `output_pydantic` attribute (it belongs to the `Task`, which Agent Kernel builds per run). | | **Google ADK `LiteLlm`** | Wrap the model string: `LiteLlm(model="openai/gpt-4o-mini")`. A bare string won't work. | | **Env var nesting** | Use `__` (double underscore) as the nested delimiter: `AK_REDIS__URL`, `AK_WHATSAPP__ACCESS_TOKEN`. | | **Single Module** | Only one Module instance per framework. Add new agents to the existing Module's agent list. | | **Single `bind()`** | Add new tools to the existing `ToolBuilder.bind()` call for that agent. Don't create a second one. | --- ### What to Do Next Now that you've added new tools and agents to your project, here are natural next steps: - **Add more tools & agents** → Use this `ak-build` skill again (it's meant to be used repeatedly) - **Add guardrails, tracing, or sessions** → Use the `ak-add-capabilities` skill to add input/output guardrails (OpenAI, Bedrock, Walled AI), observability tracing (Langfuse, OpenLLMetry, Logfire, CloudWatch), session persistence (Redis, DynamoDB, Cosmos DB), MCP server, A2A protocol, custom hooks, or multimodal support - **Connect a messaging platform** → Use the `ak-add-integration` skill to add Slack, WhatsApp, Messenger, Instagram, Telegram, or Gmail - **Deploy to cloud** → Use the `ak-cloud-deploy` skill to deploy to AWS Lambda, AWS ECS/Fargate, Azure Functions, or Azure Container Apps with Terraform - **Set up testing** → Use the `ak-test` skill to configure test modes (score, llm, fallback), write agent tests, and debug common issues