Skip to main content

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 versionThis repo (SDK)
Agent loopWritten by hand (~50 lines)SDK handles it (zero lines)
LLM callsDirect to Azure OpenAISDK → Copilot CLI → GitHub service
Tool definitionstools.json + dispatch dicttools.json → SDK Tool objects
Tool executionOur handler functionsSame handlers, SDK calling convention
PermissionsOur check_permission()PermissionHandler.approve_all
MCPOur McpSession classmcp_servers={} config dict
SkillsOur prompt.py loads skills/*.mdskill_directories=[] parameter
AuthAzure Entra IDGitHub 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.