Agent Harness (Copilot SDK)
Same demo as agent-harness, but the GitHub Copilot SDK handles the agent loop. Compare side-by-side to see what the SDK abstracts away.
:::info The comparison is the demo Run both repos on the same prompt. Same behavior, 1/3 the code. The difference is that the SDK hides the agent loop — the part where the model calls tools, we execute them, and feed results back. :::
What's Different
| DIY version | This repo (SDK) | |
|---|---|---|
| Agent loop | Written by hand (~50 lines) | SDK handles it (zero lines) |
| LLM calls | Direct to Azure OpenAI | SDK → Copilot CLI → GitHub service |
| Tool definitions | tools.json + dispatch dict | tools.json → SDK Tool objects |
| Tool execution | Our handler functions | Same handlers, SDK calling convention |
| Permissions | Our check_permission() | PermissionHandler.approve_all |
| MCP | Our McpSession class | mcp_servers={} config dict |
| Skills | Our prompt.py loads skills/*.md | skill_directories=[] parameter |
| Auth | Azure Entra ID | GitHub token |
| Python LOC | ~350 | ~120 |
Setup
# Prerequisites: Python 3.11+, GitHub Copilot subscription
pip install -e .
# Copy config and set your GitHub token
cp config.example.json config.json
Set your token in config.json:
{
"github_token": "ghp_your_token_here"
}
Or use an env var:
export GITHUB_TOKEN=ghp_...
Running the Demo
python -m agent_harness_sdk
Same edit-and-rerun workflow — modify tools.json, skills/, or config.json in VS Code.
How Tools Work in the SDK
In the DIY version, we write a handler dispatch table mapping tool names to functions. In the SDK version, we use the Tool dataclass:
from copilot.tools import Tool, ToolInvocation, ToolResult
Tool(
name="read_file",
description="Read a text file from the workspace.",
handler=_handle_read_file,
parameters={"type": "object", "properties": {"path": {"type": "string"}}},
)
The handler receives a ToolInvocation and returns a ToolResult:
def _handle_read_file(invocation: ToolInvocation) -> ToolResult:
path = invocation.arguments.get("path", "")
text = Path(path).read_text()
return ToolResult(text_result_for_llm=text)
The SDK calls our handler when the model requests the tool. We never see the agent loop.
How MCP Works in the SDK
In the DIY version, we wrote a McpSession context manager that manages the stdio connection. In the SDK, it's a config dict:
session = await client.create_session(
mcp_servers={
"mcp_server": {
"command": "python",
"args": ["-m", "mcp_server"],
"tools": ["*"],
}
}
)
Same MCP server, same protocol. The SDK manages the connection lifecycle.
How Skills Work in the SDK
In the DIY version, we read skills/*.md and append to the system prompt. In the SDK, it's a parameter:
session = await client.create_session(
skill_directories=["skills"]
)
The SDK reads the skill files and injects them into the prompt. Same concept, zero code.
Architecture
src/
├── agent_harness_sdk/ # the SDK-based agent
│ ├── agent.py # REPL — delegates to CopilotClient/CopilotSession
│ └── tools.py # load tools.json → SDK Tool objects + handlers
└── mcp_server/ # standalone MCP server (identical to DIY version)
└── __main__.py
┌────────────┐ ┌──────────────┐ JSON-RPC ┌──────────────┐ ┌──────────┐
│ User │────▶│ agent.py │───────────▶│ Copilot CLI │────▶│ GitHub │
│ (terminal)│ │ (SDK client)│◀───────────│ (subprocess)│◀────│ Service │
└────────────┘ └──────┬───────┘ events └──────┬───────┘ └──────────┘
│ │
│ ┌──────────┼──────────┐
│ ▼ ▼ ▼
└───────────▶ tools.json skills/ mcp_server
The Teaching Point
The agent loop is the only thing that makes an LLM into an agent. The DIY version shows it explicitly. This version hides it in the SDK. Both produce the same result — which proves that the concepts (tools, skills, MCP, permissions) are universal. The SDK is just a packaging of those concepts.