Skip to content
Merged
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
13 changes: 9 additions & 4 deletions python/packages/kagent-adk/src/kagent/adk/_a2a.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import faulthandler
import logging
import os
from typing import Callable
from typing import Callable, List

import httpx
from a2a.server.apps import A2AFastAPIApplication
Expand All @@ -14,8 +14,11 @@
from fastapi.responses import PlainTextResponse
from google.adk.agents import BaseAgent
from google.adk.apps import App
from google.adk.plugins import BasePlugin
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.adk.artifacts import InMemoryArtifactService

from google.genai import types

from kagent.core.a2a import KAgentRequestContextBuilder, KAgentTaskStore
Expand Down Expand Up @@ -64,11 +67,13 @@ def __init__(
agent_card: AgentCard,
kagent_url: str,
app_name: str,
plugins: List[BasePlugin] = None,
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using mutable default argument None is correct, but the parameter type annotation should be Optional[List[BasePlugin]] or List[BasePlugin] | None for clarity and to match the pattern used elsewhere in the codebase (e.g., line 76 where it's checked for None).

Copilot uses AI. Check for mistakes.
):
self.root_agent = root_agent
self.kagent_url = kagent_url
self.app_name = app_name
self.agent_card = agent_card
self.plugins = plugins if plugins is not None else []

def build(self) -> FastAPI:
token_service = KAgentTokenService(self.app_name)
Expand All @@ -77,17 +82,17 @@ def build(self) -> FastAPI:
)
session_service = KAgentSessionService(http_client)

plugins = []
if sts_well_known_uri:
sts_integration = ADKSTSIntegration(sts_well_known_uri)
plugins.append(ADKTokenPropagationPlugin(sts_integration))
self.plugins.append(ADKTokenPropagationPlugin(sts_integration))

adk_app = App(name=self.app_name, root_agent=self.root_agent, plugins=plugins)
adk_app = App(name=self.app_name, root_agent=self.root_agent, plugins=self.plugins)

def create_runner() -> Runner:
return Runner(
app=adk_app,
session_service=session_service,
artifact_service=InMemoryArtifactService(),
)

agent_executor = A2aAgentExecutor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from .converters.event_converter import convert_event_to_a2a_events
from .converters.request_converter import convert_a2a_request_to_adk_run_args

logger = logging.getLogger("google_adk." + __name__)
logger = logging.getLogger("kagent_adk." + __name__)


class A2aAgentExecutorConfig(BaseModel):
Expand Down
217 changes: 217 additions & 0 deletions python/packages/kagent-adk/src/kagent/adk/skills/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
# ADK Skills

Filesystem-based skills with progressive disclosure and two-tool architecture.

---

## Overview

Skills enable agents to specialize in domain expertise without bloating the main context. The **two-tool pattern** separates concerns:

- **SkillsTool** - Loads skill instructions
- **BashTool** - Executes commands
- **Semantic clarity** leads to better LLM reasoning

### Skill Structure

```text
skills/
├── data-analysis/
│ ├── SKILL.md # Metadata + instructions (YAML frontmatter)
│ └── scripts/
│ └── analyze.py
└── pdf-processing/
├── SKILL.md
└── scripts/
```

**SKILL.md:**

```markdown
---
name: data-analysis
description: Analyze CSV/Excel files
---

# Data Analysis

...instructions...
```

---

## Quick Start

**Two-Tool Pattern (Recommended):**

```python
from kagent.adk.skills import SkillsTool, BashTool, StageArtifactsTool

agent = Agent(
tools=[
SkillsTool(skills_directory="./skills"),
BashTool(skills_directory="./skills"),
StageArtifactsTool(skills_directory="./skills"),
]
)
```

**With Plugin (Multi-Agent Apps):**

```python
from kagent.adk.skills import SkillsPlugin

app = App(root_agent=agent, plugins=[SkillsPlugin(skills_directory="./skills")])
```

**Legacy Single-Tool (Backward Compat):**

```python
from kagent.adk.skills import SkillsShellTool

agent = Agent(tools=[SkillsShellTool(skills_directory="./skills")])
```

---

## How It Works

### Two-Tool Workflow

```mermaid
sequenceDiagram
participant A as Agent
participant S as SkillsTool
participant B as BashTool

A->>S: skills(command='data-analysis')
S-->>A: Full SKILL.md + base path
A->>B: bash("cd skills/data-analysis && python scripts/analyze.py file.csv")
B-->>A: Results
```

**Three Phases:**

1. **Discovery** - Agent sees available skills in tool description
2. **Loading** - Invoke skill with `command='skill-name'` → returns full SKILL.md
3. **Execution** - Use BashTool with instructions from SKILL.md

---

## Architecture

```mermaid
graph LR
Agent[Agent] -->|Load<br/>skill details| SkillsTool["SkillsTool<br/>(Discovery)"]
Agent -->|Execute<br/>commands| BashTool["BashTool<br/>(Execution)"]
SkillsTool -->|Embedded in<br/>description| Skills["Available<br/>Skills List"]
```

| Tool | Purpose | Input | Output |
| ---------------------- | ------------------- | ---------------------- | ------------------------- |
| **SkillsTool** | Load skill metadata | `command='skill-name'` | Full SKILL.md + base path |
| **BashTool** | Execute safely | Command string | Script output |
| **StageArtifactsTool** | Stage uploads | Artifact names | File paths in `uploads/` |

---

## File Handling

User uploads → Artifact → Stage → Execute:

```python
# 1. Stage uploaded file
stage_artifacts(artifact_names=["artifact_123"])

# 2. Use in skill script
bash("cd skills/data-analysis && python scripts/analyze.py uploads/artifact_123")
```

---

## Security

**SkillsTool:**

- ✅ Read-only (no execution)
- ✅ Validates skill existence
- ✅ Caches results

**BashTool:**

- ✅ Whitelisted commands only (`ls`, `cat`, `python`, `pip`, etc.)
- ✅ No destructive ops (`rm`, `mv`, `chmod` blocked)
- ✅ Directory restrictions (no `..`)
- ✅ 30-second timeout
- ✅ Subprocess isolation

---

## Components

| File | Purpose |
| ------------------------- | ---------------------------- |
| `skills_invoke_tool.py` | Discovery & loading |
| `bash_tool.py` | Command execution |
| `stage_artifacts_tool.py` | File staging |
| `skills_plugin.py` | Auto-registration (optional) |
| `skills_shell_tool.py` | Legacy all-in-one |

---

## Examples

### Example 1: Data Analysis

```python
# Agent loads skill
agent.invoke(tools=[
SkillsTool(skills_directory="./skills"),
BashTool(skills_directory="./skills"),
], prompt="Analyze this CSV file")

# Agent flow:
# 1. Calls: skills(command='data-analysis')
# 2. Gets: Full SKILL.md with instructions
# 3. Calls: bash("cd skills/data-analysis && python scripts/analyze.py file.csv")
# 4. Returns: Analysis results
```

### Example 2: Multi-Agent App

```python
# Register skills on all agents
app = App(
root_agent=agent,
plugins=[SkillsPlugin(skills_directory="./skills")]
)
```

---

## Comparison with Claude

ADK follows Claude's two-tool pattern exactly:

| Aspect | Claude | ADK |
| -------------- | ------------------- | ---------------------- |
| Discovery tool | Skills tool | SkillsTool ✅ |
| Execution tool | Bash tool | BashTool ✅ |
| Parameter | `command` | `command` ✅ |
| Pattern | Two-tool separation | Two-tool separation ✅ |

---

## What Changed

**Before:** Single `SkillsShellTool` (all-in-one)
**Now:** Two-tool architecture (discovery + execution)

| Feature | Before | After |
| ---------------------- | --------- | ----------------- |
| Semantic clarity | Mixed | Separated ✅ |
| LLM reasoning | Implicit | Explicit ✅ |
| Progressive disclosure | Guideline | Enforced ✅ |
| Industry alignment | Custom | Claude pattern ✅ |

All previous code still works (backward compatible via `SkillsShellTool`).
29 changes: 29 additions & 0 deletions python/packages/kagent-adk/src/kagent/adk/skills/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from .bash_tool import BashTool
from .skill_system_prompt import generate_shell_skills_system_prompt
from .skill_tool import SkillsTool
from .skills_plugin import SkillsPlugin
from .skills_toolset import SkillsToolset
from .stage_artifacts_tool import StageArtifactsTool

__all__ = [
"BashTool",
"SkillsTool",
"SkillsPlugin",
"SkillsToolset",
"StageArtifactsTool",
"generate_shell_skills_system_prompt",
]
Loading
Loading