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
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

## Project-specific conventions & patterns ✅

- Tools: each SDK has helper APIs to expose functions as tools; prefer the language's `DefineTool`/`@define_tool`/`AIFunctionFactory.Create` patterns (see language READMEs).
- Tools: each SDK has helper APIs to expose functions as tools; prefer the language's `DefineTool`/`@define_tool`/`CopilotTool.DefineTool` patterns (see language READMEs).
- Infinite sessions are enabled by default and persist workspace state to `~/.copilot/session-state/{sessionId}`; compaction events are emitted (`session.compaction_start`, `session.compaction_complete`). See language READMEs for usage.
- Streaming: when `streaming`/`Streaming=true` you receive delta events (`assistant.message_delta`, `assistant.reasoning_delta`) and final events (`assistant.message`, `assistant.reasoning`) — tests expect this behavior.
- Type generation is centralized in `nodejs/scripts/generate-session-types.ts` and requires the `@github/copilot` schema to be present (often via `npm link` or installed package).
Expand Down
20 changes: 13 additions & 7 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -1166,7 +1166,7 @@ using System.ComponentModel;
await using var client = new CopilotClient();

// Define a tool that Copilot can call
var getWeather = AIFunctionFactory.Create(
var getWeather = CopilotTool.DefineTool(
([Description("The city name")] string city) =>
{
// In a real app, you'd call a weather API here
Expand All @@ -1175,8 +1175,11 @@ var getWeather = AIFunctionFactory.Create(
var condition = conditions[Random.Shared.Next(conditions.Length)];
return new { city, temperature = $"{temp}°F", condition };
},
"get_weather",
"Get the current weather for a city"
factoryOptions: new AIFunctionFactoryOptions
{
Name = "get_weather",
Description = "Get the current weather for a city",
}
);

await using var session = await client.CreateSessionAsync(new SessionConfig
Expand Down Expand Up @@ -1648,17 +1651,20 @@ using GitHub.Copilot.SDK;
using Microsoft.Extensions.AI;
using System.ComponentModel;

// Define the weather tool using AIFunctionFactory
var getWeather = AIFunctionFactory.Create(
// Define the weather tool
var getWeather = CopilotTool.DefineTool(
([Description("The city name")] string city) =>
{
var conditions = new[] { "sunny", "cloudy", "rainy", "partly cloudy" };
var temp = Random.Shared.Next(50, 80);
var condition = conditions[Random.Shared.Next(conditions.Length)];
return new { city, temperature = $"{temp}°F", condition };
},
"get_weather",
"Get the current weather for a city");
factoryOptions: new AIFunctionFactoryOptions
{
Name = "get_weather",
Description = "Get the current weather for a city",
});

await using var client = new CopilotClient();
await using var session = await client.CreateSessionAsync(new SessionConfig
Expand Down
9 changes: 6 additions & 3 deletions docs/integrations/microsoft-agent-framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,13 @@ using Microsoft.Extensions.AI;
using Microsoft.Agents.AI;

// Define a custom tool
AIFunction weatherTool = AIFunctionFactory.Create(
AIFunction weatherTool = CopilotTool.DefineTool(
(string location) => $"The weather in {location} is sunny with a high of 25°C.",
"GetWeather",
"Get the current weather for a given location."
factoryOptions: new AIFunctionFactoryOptions
{
Name = "GetWeather",
Description = "Get the current weather for a given location.",
}
);

await using var copilotClient = new CopilotClient();
Expand Down
51 changes: 31 additions & 20 deletions dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ await client.StopAsync();

### Tools

You can let the CLI call back into your process when the model needs capabilities you own. Use `AIFunctionFactory.Create` from Microsoft.Extensions.AI for type-safe tool definitions:
You can let the CLI call back into your process when the model needs capabilities you own. Use `CopilotTool.DefineTool` for type-safe tool definitions:

```csharp
using Microsoft.Extensions.AI;
Expand All @@ -435,34 +435,39 @@ var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5",
Tools = [
AIFunctionFactory.Create(
CopilotTool.DefineTool(
async ([Description("Issue identifier")] string id) => {
var issue = await FetchIssueAsync(id);
return issue;
},
"lookup_issue",
"Fetch issue details from our tracker"),
factoryOptions: new AIFunctionFactoryOptions
{
Name = "lookup_issue",
Description = "Fetch issue details from our tracker",
}),
]
});
```

When Copilot invokes `lookup_issue`, the client automatically runs your handler and responds to the CLI. Handlers can return any JSON-serializable value (automatically wrapped), or a `ToolResultAIContent` wrapping a `ToolResultObject` for full control over result metadata.
When Copilot invokes `lookup_issue`, the client automatically runs your handler and responds to the CLI. Handlers can return any JSON-serializable value (automatically wrapped), or a `ToolResultAIContent` wrapping a `ToolResultObject` for full control over result metadata. Include a `ToolInvocation` parameter in your handler if you need the session ID, tool call ID, tool name, or raw arguments.

#### Overriding Built-in Tools

If you register a tool with the same name as a built-in CLI tool (e.g. `edit_file`, `read_file`), the runtime will return an error unless you explicitly opt in by setting `is_override` in the tool's `AdditionalProperties`. This flag signals that you intend to replace the built-in tool with your custom implementation.
If you register a tool with the same name as a built-in CLI tool (e.g. `edit_file`, `read_file`), the runtime will return an error unless you explicitly opt in with `CopilotToolOptions.OverridesBuiltInTool`. This flag signals that you intend to replace the built-in tool with your custom implementation.

```csharp
var editFile = AIFunctionFactory.Create(
var editFile = CopilotTool.DefineTool(
async ([Description("File path")] string path, [Description("New content")] string content) => {
// your logic
},
"edit_file",
"Custom file editor with project-specific validation",
new AIFunctionFactoryOptions
toolOptions: new CopilotToolOptions
{
OverridesBuiltInTool = true
},
factoryOptions: new AIFunctionFactoryOptions
{
AdditionalProperties = new ReadOnlyDictionary<string, object?>(
new Dictionary<string, object?> { ["is_override"] = true })
Name = "edit_file",
Description = "Custom file editor with project-specific validation",
});

var session = await client.CreateSessionAsync(new SessionConfig
Expand All @@ -474,22 +479,28 @@ var session = await client.CreateSessionAsync(new SessionConfig

#### Skipping Permission Prompts

Set `skip_permission` in the tool's `AdditionalProperties` to allow it to execute without triggering a permission prompt:
Set `CopilotToolOptions.SkipPermission` to allow a tool to execute without triggering a permission prompt:

```csharp
var safeLookup = AIFunctionFactory.Create(
var safeLookup = CopilotTool.DefineTool(
async ([Description("Lookup ID")] string id) => {
// your logic
},
"safe_lookup",
"A read-only lookup that needs no confirmation",
new AIFunctionFactoryOptions
toolOptions: new CopilotToolOptions
{
AdditionalProperties = new ReadOnlyDictionary<string, object?>(
new Dictionary<string, object?> { ["skip_permission"] = true })
SkipPermission = true
},
factoryOptions: new AIFunctionFactoryOptions
{
Name = "safe_lookup",
Description = "A read-only lookup that needs no confirmation",
});
```

`DefineTool` delegates to `AIFunctionFactory.Create`, so advanced `AIFunctionFactoryOptions` remain available through the overload that accepts both `AIFunctionFactoryOptions` and `CopilotToolOptions`.

If you want to use `AIFunctionFactory.Create` directly, you can set `skip_permission` in the tool's `AdditionalProperties`.

## Commands

Register slash commands so that users of the CLI's TUI can invoke custom actions via `/commandName`. Each command has a `Name`, optional `Description`, and a `Handler` called when the user executes it.
Expand Down Expand Up @@ -789,7 +800,7 @@ var session = await client.ResumeSessionAsync("session-id", new ResumeSessionCon

### Per-Tool Skip Permission

To let a specific custom tool bypass the permission prompt entirely, set `skip_permission = true` in the tool's `AdditionalProperties`. See [Skipping Permission Prompts](#skipping-permission-prompts) under Tools.
To let a specific custom tool bypass the permission prompt entirely, set `SkipPermission = true` in `CopilotToolOptions`. See [Skipping Permission Prompts](#skipping-permission-prompts) under Tools.

## User Input Requests

Expand Down
4 changes: 2 additions & 2 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1995,8 +1995,8 @@ internal record ToolDefinition(
{
public static ToolDefinition FromAIFunction(AIFunction function)
{
var overrides = function.AdditionalProperties.TryGetValue("is_override", out var val) && val is true;
var skipPerm = function.AdditionalProperties.TryGetValue("skip_permission", out var skipVal) && skipVal is true;
var overrides = function.AdditionalProperties.TryGetValue(CopilotTool.OverridesBuiltInToolKey, out var val) && val is true;
var skipPerm = function.AdditionalProperties.TryGetValue(CopilotTool.SkipPermissionKey, out var skipVal) && skipVal is true;
return new ToolDefinition(function.Name, function.Description, function.JsonSchema,
overrides ? true : null,
skipPerm ? true : null);
Expand Down
137 changes: 137 additions & 0 deletions dotnet/src/CopilotTool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

using Microsoft.Extensions.AI;

namespace GitHub.Copilot.SDK;

/// <summary>
/// Provides helpers for defining Copilot tools.
/// </summary>
public static class CopilotTool
{
/// <summary>The key used in <see cref="AITool.AdditionalProperties"/> to indicate that a tool intentionally overrides a built-in Copilot tool with the same name.</summary>
internal const string OverridesBuiltInToolKey = "is_override";

/// <summary>The key used in <see cref="AITool.AdditionalProperties"/> to indicate that a tool can execute without a permission prompt.</summary>
internal const string SkipPermissionKey = "skip_permission";

/// <summary>
/// Defines a tool for use in a <see cref="CopilotSession"/>.
/// </summary>
/// <param name="method">The delegate to invoke when the tool is called.</param>
/// <param name="factoryOptions">The Microsoft.Extensions.AI options used to create the function.</param>
/// <param name="toolOptions">Copilot-specific tool options.</param>
/// <returns>An <see cref="AIFunction"/> that can be added to <see cref="SessionConfig.Tools"/> or <see cref="ResumeSessionConfig.Tools"/>.</returns>
/// <remarks>
/// This is a helper on top of <see cref="AIFunctionFactory.Create(Delegate, AIFunctionFactoryOptions)"/> that applies additional configuration to support
/// Copilot tools, such as binding a <see cref="ToolInvocation"/> parameter and adding Copilot-specific metadata properties based on the provided
/// <see cref="CopilotToolOptions"/>. Any <see cref="AIFunction"/> may be used as a Copilot tool; this helper simply provides additional conveniences
/// for tools that opt in to advanced features.
/// </remarks>
public static AIFunction DefineTool(
Delegate method,
CopilotToolOptions? toolOptions = null,
AIFunctionFactoryOptions? factoryOptions = null)
{
ArgumentNullException.ThrowIfNull(method);

factoryOptions ??= new();

ApplyToolOptions(factoryOptions, toolOptions);
ApplyToolInvocationBinding(factoryOptions);

return AIFunctionFactory.Create(method, factoryOptions);

static void ApplyToolInvocationBinding(AIFunctionFactoryOptions factoryOptions)
{
var configureParameterBinding = factoryOptions.ConfigureParameterBinding;
factoryOptions.ConfigureParameterBinding = pi =>
{
var bindingOptions = configureParameterBinding?.Invoke(pi) ?? default;

if (bindingOptions.BindParameter is null &&
!bindingOptions.ExcludeFromSchema &&
pi.ParameterType == typeof(ToolInvocation))
{
return new AIFunctionFactoryOptions.ParameterBindingOptions
{
ExcludeFromSchema = true,
BindParameter = static (pi, arguments) =>
{
// CopilotClient/CopilotSession attach this context object before invoking the AIFunction.
if (arguments.Context is not null &&
arguments.Context.TryGetValue(typeof(ToolInvocation), out var invocation) &&
invocation is ToolInvocation toolInvocation)
{
return toolInvocation;
}

if (pi.HasDefaultValue)
{
return null;
}

throw new InvalidOperationException($"No {nameof(ToolInvocation)} was provided for the tool call.");
}
};
}

return bindingOptions;
};
}

static void ApplyToolOptions(AIFunctionFactoryOptions factoryOptions, CopilotToolOptions? toolOptions)
{
if (toolOptions is not null && (toolOptions.OverridesBuiltInTool || toolOptions.SkipPermission))
{
Dictionary<string, object?> additionalProperties = new(StringComparer.Ordinal);
if (factoryOptions.AdditionalProperties is not null)
{
foreach (var (key, value) in factoryOptions.AdditionalProperties)
{
additionalProperties[key] = value;
}
}

if (toolOptions.OverridesBuiltInTool)
{
additionalProperties[OverridesBuiltInToolKey] = true;
}

if (toolOptions.SkipPermission)
{
additionalProperties[SkipPermissionKey] = true;
}

factoryOptions.AdditionalProperties = additionalProperties;
}
}
}

}

/// <summary>
/// Copilot-specific options for tools defined with <see cref="CopilotTool"/>.
/// </summary>
public sealed class CopilotToolOptions
{
/// <summary>
/// Gets or sets a value indicating whether this tool intentionally overrides a built-in Copilot tool with the same name.
/// </summary>
/// <remarks>
/// When a <see cref="CopilotToolOptions"/> with <see cref="OverridesBuiltInTool"/> set to true is used to define a tool,
/// the resulting <see cref="AIFunction"/> will include "is_override": true in its <see cref="AITool.AdditionalProperties"/>.
/// </remarks>
public bool OverridesBuiltInTool { get; set; }

/// <summary>
/// Gets or sets a value indicating whether this tool can execute without a permission prompt.
/// </summary>
/// <remarks>
/// When a <see cref="CopilotToolOptions"/> with <see cref="SkipPermission"/> set to true is used to define a tool,
/// the resulting <see cref="AIFunction"/> will include "skip_permission": true in its <see cref="AITool.AdditionalProperties"/>.
/// </remarks>
public bool SkipPermission { get; set; }
}
Loading
Loading