| name | copilot-sdk |
| description | Integration with @github/copilot-sdk for AI-powered agent sessions. Use when implementing Copilot chat, streaming responses, tool definitions, function calling, or managing agent conversations. Triggers on copilot, copilot-sdk, agent sessions, AI streaming, tool calls, function calling, chat completions. |
GitHub Copilot SDK Integration
Integrate @github/copilot-sdk for AI-powered agent sessions with streaming responses and tool calling.
When to Use This Skill
- Setting up Copilot SDK in Electron main process
- Implementing streaming chat completions
- Defining tools for agent function calling
- Managing conversation context
- Handling tool execution results
- Building decision points with user approval
SDK Setup
Installation and Authentication
import { CopilotClient } from '@github/copilot-sdk';
let client: CopilotClient | null = null;
export async function initializeCopilotClient(): Promise<CopilotClient> {
if (client) return client;
client = new CopilotClient({
});
return client;
}
export function getCopilotClient(): CopilotClient {
if (!client) {
throw new Error('Copilot client not initialized. Call initializeCopilotClient first.');
}
return client;
}
Tool Definitions
import type { Tool, ToolResult } from '@github/copilot-sdk';
export const agentTools: Tool[] = [
{
name: 'create_file',
description: 'Create a new file with the specified content',
parameters: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'The file path relative to the project root',
},
content: {
type: 'string',
description: 'The content to write to the file',
},
},
required: ['path', 'content'],
},
},
{
name: 'edit_file',
description: 'Edit an existing file by replacing content',
parameters: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'The file path relative to the project root',
},
search: {
type: 'string',
description: 'The exact text to find and replace',
},
replace: {
type: 'string',
description: 'The replacement text',
},
},
required: ['path', 'search', 'replace'],
},
},
{
name: 'run_command',
description: 'Execute a shell command in the project directory',
parameters: {
type: 'object',
properties: {
command: {
type: 'string',
description: 'The shell command to execute',
},
cwd: {
type: 'string',
description: 'Working directory (optional)',
},
},
required: ['command'],
},
},
{
name: 'read_file',
description: 'Read the contents of a file',
parameters: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'The file path to read',
},
},
required: ['path'],
},
},
{
name: 'request_approval',
description: 'Request user approval before proceeding with a sensitive action',
parameters: {
type: 'object',
properties: {
action: {
type: 'string',
description: 'Description of the action requiring approval',
},
details: {
type: 'string',
description: 'Detailed explanation of what will happen',
},
risk_level: {
type: 'string',
enum: ['low', 'medium', 'high'],
description: 'Risk level of the action',
},
},
required: ['action', 'details'],
},
},
];
Tool Execution
import type { ToolCall, ToolResult } from '@github/copilot-sdk';
import { readFile, writeFile } from 'fs/promises';
import { exec } from 'child_process';
import { promisify } from 'util';
import path from 'path';
const execAsync = promisify(exec);
interface ExecutionContext {
projectRoot: string;
onApprovalRequest: (
action: string,
details: string,
riskLevel?: string
) => Promise<boolean>;
}
export async function executeToolCall(
toolCall: ToolCall,
context: ExecutionContext
): Promise<ToolResult> {
const { name, arguments: args } = toolCall;
try {
switch (name) {
case 'create_file': {
const filePath = path.resolve(context.projectRoot, args.path);
await writeFile(filePath, args.content, 'utf-8');
return {
success: true,
result: `Created file: ${args.path}`,
};
}
case 'edit_file': {
const filePath = path.resolve(context.projectRoot, args.path);
const content = await readFile(filePath, 'utf-8');
if (!content.includes(args.search)) {
return {
success: false,
error: `Search text not found in ${args.path}`,
};
}
const newContent = content.replace(args.search, args.replace);
await writeFile(filePath, newContent, 'utf-8');
return {
success: true,
result: `Edited file: ${args.path}`,
};
}
case 'run_command': {
const cwd = args.cwd
? path.resolve(context.projectRoot, args.cwd)
: context.projectRoot;
const { stdout, stderr } = await execAsync(args.command, {
cwd,
timeout: 30000,
});
return {
success: true,
result: stdout || stderr || 'Command completed',
};
}
case 'read_file': {
const filePath = path.resolve(context.projectRoot, args.path);
const content = await readFile(filePath, 'utf-8');
return {
success: true,
result: content,
};
}
case 'request_approval': {
const approved = await context.onApprovalRequest(
args.action,
args.details,
args.risk_level
);
return {
success: true,
result: approved ? 'APPROVED' : 'DENIED',
};
}
default:
return {
success: false,
error: `Unknown tool: ${name}`,
};
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
Agent Session Management
Session Runner
import { getCopilotClient } from './client';
import { agentTools } from './tools';
import { executeToolCall } from './tool-executor';
import type {
Message,
StreamEvent,
ToolCall,
} from '@github/copilot-sdk';
import type { AgentSession, AgentStep } from '@agentop/core';
import { db } from '../database';
import { nanoid } from 'nanoid';
interface SessionConfig {
outcomeId: string;
systemPrompt: string;
userMessage: string;
projectRoot: string;
attentionType: 'supervised' | 'delegated' | 'sensitive';
onStep: (step: AgentStep) => void;
onApprovalRequest: (
action: string,
details: string,
riskLevel?: string
) => Promise<boolean>;
}
export async function runAgentSession(config: SessionConfig): Promise<AgentSession> {
const client = getCopilotClient();
const sessionId = nanoid();
const session: AgentSession = {
id: sessionId,
outcomeId: config.outcomeId,
status: 'running',
startedAt: new Date(),
attentionType: config.attentionType,
};
await db.insert('agent_sessions', session);
const messages: Message[] = [
{ role: 'system', content: config.systemPrompt },
{ role: 'user', content: config.userMessage },
];
try {
await runConversationLoop({
client,
sessionId,
messages,
config,
});
session.status = 'completed';
session.completedAt = new Date();
await db.update('agent_sessions', sessionId, session);
return session;
} catch (error) {
session.status = 'error';
session.error = error instanceof Error ? error.message : 'Unknown error';
session.completedAt = new Date();
await db.update('agent_sessions', sessionId, session);
throw error;
}
}
interface ConversationLoopConfig {
client: CopilotClient;
sessionId: string;
messages: Message[];
config: SessionConfig;
}
async function runConversationLoop({
client,
sessionId,
messages,
config,
}: ConversationLoopConfig): Promise<void> {
let continueLoop = true;
while (continueLoop) {
const stream = await client.chat.completions.create({
model: 'gpt-4',
messages,
tools: agentTools,
stream: true,
});
let assistantContent = '';
let toolCalls: ToolCall[] = [];
let stepId = nanoid();
for await (const event of stream) {
switch (event.type) {
case 'content_delta':
assistantContent += event.delta;
config.onStep({
id: stepId,
sessionId,
type: 'thinking',
status: 'in_progress',
content: assistantContent,
createdAt: new Date(),
});
break;
case 'tool_call':
toolCalls.push(event.toolCall);
break;
case 'done':
break;
}
}
messages.push({
role: 'assistant',
content: assistantContent,
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
});
const thinkingStep: AgentStep = {
id: stepId,
sessionId,
type: 'thinking',
status: 'completed',
content: assistantContent,
createdAt: new Date(),
};
await db.insert('agent_steps', thinkingStep);
config.onStep(thinkingStep);
if (toolCalls.length > 0) {
for (const toolCall of toolCalls) {
const toolStepId = nanoid();
const needsApproval =
config.attentionType === 'supervised' ||
(config.attentionType === 'sensitive' &&
toolCall.name === 'request_approval');
if (needsApproval && toolCall.name !== 'request_approval') {
const decisionStep: AgentStep = {
id: toolStepId,
sessionId,
type: 'decision_point',
status: 'pending',
toolCall,
createdAt: new Date(),
};
await db.insert('agent_steps', decisionStep);
config.onStep(decisionStep);
const approved = await config.onApprovalRequest(
`Execute tool: ${toolCall.name}`,
JSON.stringify(toolCall.arguments, null, 2),
'medium'
);
if (!approved) {
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: 'Tool execution denied by user',
});
decisionStep.status = 'denied';
await db.update('agent_steps', toolStepId, decisionStep);
config.onStep(decisionStep);
continue;
}
decisionStep.status = 'approved';
await db.update('agent_steps', toolStepId, decisionStep);
config.onStep(decisionStep);
}
const actionStep: AgentStep = {
id: nanoid(),
sessionId,
type: 'action',
status: 'in_progress',
toolCall,
createdAt: new Date(),
};
config.onStep(actionStep);
const result = await executeToolCall(toolCall, {
projectRoot: config.projectRoot,
onApprovalRequest: config.onApprovalRequest,
});
actionStep.status = result.success ? 'completed' : 'error';
actionStep.result = result;
await db.insert('agent_steps', actionStep);
config.onStep(actionStep);
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: result.success
? result.result
: `Error: ${result.error}`,
});
}
} else {
continueLoop = false;
}
}
}
IPC Handlers
import { ipcMain, BrowserWindow } from 'electron';
import { runAgentSession } from './session';
import type { AgentStep } from '@agentop/core';
export function registerCopilotHandlers() {
ipcMain.handle(
'copilot:session:start',
async (event, config) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (!win) throw new Error('No window');
return runAgentSession({
...config,
onStep: (step: AgentStep) => {
win.webContents.send('copilot:session:step', step);
},
onApprovalRequest: async (action, details, riskLevel) => {
win.webContents.send('copilot:approval:request', {
action,
details,
riskLevel,
});
return new Promise((resolve) => {
ipcMain.once('copilot:approval:response', (_, approved) => {
resolve(approved);
});
});
},
});
}
);
ipcMain.handle('copilot:session:cancel', async (_, sessionId) => {
});
}
Renderer Integration
Session Hook
import { useState, useCallback, useEffect } from 'react';
import { useSetAtom } from 'jotai';
import { stepsMapAtom, sessionAtomFamily } from '@/state/atoms';
import type { AgentSession, AgentStep } from '@agentop/core';
interface StartSessionInput {
outcomeId: string;
systemPrompt: string;
userMessage: string;
attentionType: 'supervised' | 'delegated' | 'sensitive';
}
export function useAgentSession() {
const [isRunning, setIsRunning] = useState(false);
const [pendingApproval, setPendingApproval] = useState<{
action: string;
details: string;
riskLevel?: string;
} | null>(null);
const setSteps = useSetAtom(stepsMapAtom);
useEffect(() => {
const unsubscribe = window.api.on(
'copilot:session:step',
(step: AgentStep) => {
setSteps((map) => {
const newMap = new Map(map);
newMap.set(step.id, step);
return newMap;
});
}
);
return unsubscribe;
}, [setSteps]);
useEffect(() => {
const unsubscribe = window.api.on(
'copilot:approval:request',
(request) => {
setPendingApproval(request);
}
);
return unsubscribe;
}, []);
const startSession = useCallback(
async (input: StartSessionInput): Promise<AgentSession> => {
setIsRunning(true);
try {
const session = await window.api.invoke(
'copilot:session:start',
input
);
return session;
} finally {
setIsRunning(false);
}
},
[]
);
const respondToApproval = useCallback((approved: boolean) => {
window.api.send('copilot:approval:response', approved);
setPendingApproval(null);
}, []);
return {
startSession,
isRunning,
pendingApproval,
approveAction: () => respondToApproval(true),
denyAction: () => respondToApproval(false),
};
}
Approval Dialog
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { AlertTriangle, CheckCircle, XCircle } from 'lucide-react';
import { useAgentSession } from '@/hooks/useAgentSession';
export function ApprovalDialog() {
const { pendingApproval, approveAction, denyAction } = useAgentSession();
if (!pendingApproval) return null;
const riskColors = {
low: 'text-green-500',
medium: 'text-yellow-500',
high: 'text-red-500',
};
return (
<Dialog open onOpenChange={(open) => !open && denyAction()}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<AlertTriangle
className={riskColors[pendingApproval.riskLevel || 'medium']}
/>
Action Approval Required
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<h4 className="font-medium">Action</h4>
<p className="text-muted-foreground">
{pendingApproval.action}
</p>
</div>
<div>
<h4 className="font-medium">Details</h4>
<pre className="bg-muted p-2 rounded text-sm overflow-auto max-h-48">
{pendingApproval.details}
</pre>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={denyAction}>
<XCircle className="w-4 h-4 mr-2" />
Deny
</Button>
<Button onClick={approveAction}>
<CheckCircle className="w-4 h-4 mr-2" />
Approve
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
Best Practices
- Stream responses - Always use streaming for better UX during long completions
- Implement attention types - supervised, delegated, sensitive control approval flow
- Log all steps - Every tool call and result should be persisted
- Handle cancellation - Allow users to stop sessions mid-execution
- Timeout tool calls - Prevent hung commands from blocking sessions
- Validate tool arguments - Check inputs before execution
Troubleshooting
| Issue | Solution |
|---|
| Auth errors | Ensure GitHub Copilot subscription is active |
| Tool not found | Verify tool name matches definition exactly |
| Stream hangs | Check network, implement timeout |
| Memory leak | Clean up listeners on component unmount |