Skip to content

Commit 5b7213f

Browse files
committed
Make markdown configuration compatible with VSC chat modes
See https://code.visualstudio.com/docs/copilot/customization/custom-chat-modes (seems to have been renamed to "custom agents" in VSC insiders now). Our extensions (such as `client`, `use` and `options` can be persisted in other config files (since they are aggregated at run-time) or kept in the same agents.md file since VSCode will ignore them.
1 parent 8da697a commit 5b7213f

File tree

11 files changed

+181
-44
lines changed

11 files changed

+181
-44
lines changed

.github/agents/notes.agent.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
id: ai.agents.notes
3+
description: 'Takes notes'
4+
model: Grok Code Fast 1 (copilot)
5+
client: grok
6+
options:
7+
modelid: grok-code-fast-1
8+
tools: ['edit']
9+
use: ['tone']
10+
---
11+
# Notes Agent
12+
This agent is designed to take notes based on user input. It can capture important information, summarize discussions, and organize notes for easy retrieval later. The Notes Agent can be particularly useful in meetings, brainstorming sessions, or any scenario where capturing key points is essential.
13+
14+
It saves these notes in JSON-LD format to the file `notes.json` alongside this agent, ensuring that the notes are structured and easily accessible for future reference.

.github/agents/notes.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[
2+
{
3+
"@context": "https://schema.org",
4+
"@type": "NoteDigitalDocument",
5+
"identifier": "2025-10-29T00:00:00Z",
6+
"dateCreated": "2025-10-29",
7+
"inLanguage": "es",
8+
"text": "El usuario necesita desplegar una aplicación Expo.",
9+
"about": [
10+
"Expo",
11+
"deploy"
12+
]
13+
},
14+
{
15+
"@context": "https://schema.org",
16+
"@type": "Note",
17+
"dateCreated": "2025-10-29T12:00:00Z",
18+
"inLanguage": "es",
19+
"textOriginal": "recordar que maniana llevamos piedras",
20+
"text": "Recordar que mañana llevamos piedras",
21+
"dueDate": "2025-10-30",
22+
"tags": ["recordatorio", "piedras"],
23+
"source": {
24+
"agentFile": ".github/agents/notes.agent.md",
25+
"savedBy": "notes-agent"
26+
}
27+
}
28+
]

assets/img/agent-model.png

46.2 KB
Loading

readme.md

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,29 +133,45 @@ This can be used by leveraging [Tomlyn.Extensions.Configuration](https://www.nug
133133
> avoiding unnecessary tokens being used for indentation while allowing flexible
134134
> formatting in the config file.
135135
136-
For longer instructions, markdown format plus YAML front-matter can be used for better readability:
136+
You can also leverage the format pioneered by [VS Code Chat Modes](https://code.visualstudio.com/docs/copilot/customization/custom-chat-modes),
137+
(por "custom agents") by using markdown format plus YAML front-matter for better readability:
137138

138139
```yaml
139140
---
140141
id: ai.agents.notes
141142
description: Provides free-form memory
142143
client: grok
143-
options:
144-
modelid: grok-4-fast
144+
model: grok-4-fast
145145
---
146146
You organize and keep notes for the user.
147147
# Some header
148148
More content
149+
```
149150

150-
## Another header
151-
...
151+
Visual Studio Code will ignore the additional attributes used by this project. In particular, the `model`
152+
property is a shorthand for setting the `options.modelid`, but in our implementation, the latter takes
153+
precedence over the former, which allows you to rely on `model` to drive the VSCode testing, and the
154+
longer form for run-time with the Agents Framework:
155+
156+
```yaml
157+
---
158+
id: ai.agents.notes
159+
description: Provides free-form memory
160+
model: Grok Code Fast 1 (copilot)
161+
client: grok
162+
options:
163+
modelid: grok-code-fast-1
164+
---
165+
// Instructions
152166
```
153167

154-
Use the provided `AddInstructionsFile` extension method to load instructions from files as follows:
168+
![agent model picker](assets/img/agent-model.png)
169+
170+
Use the provided `AddAgentMarkdown` extension method to load instructions from files as follows:
155171

156172
```csharp
157173
var host = new HostApplicationBuilder(args);
158-
host.Configuration.AddInstructionsFile("notes.md", optional: false, reloadOnChange: true);
174+
host.Configuration.AddAgentMarkdown("notes.agent.md", optional: false, reloadOnChange: true);
159175
```
160176

161177
The `id` field in the front-matter is required and specifies the configuration section name, and
@@ -227,15 +243,16 @@ services.AddKeyedSingleton("get_date", AIFunctionFactory.Create(() => DateTimeOf
227243

228244
This tool will be automatically wired into any agent that uses the `timezone` context above.
229245

230-
As a shortcut when you want to just pull in a tool from DI into an agent's context without having to define an entire
231-
section just for that, you can specify the tool name directly in the `use` array:
246+
Agents themselves can also add tools from DI into an agent's context without having to define an entire
247+
section just for that, by specifying the tool name directly in the `tools` array:
232248

233249
```toml
234250
[ai.agents.support]
235251
description = "An AI agent that helps with customer support."
236252
instructions = "..."
237253
client = "grok"
238-
use = ["tone", "get_date"]
254+
use = ["tone"]
255+
tools = ["get_date"]
239256
```
240257

241258
This enables a flexible and convenient mix of static and dynamic context for agents, all driven

sample/Server/ai.toml

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,16 @@ instructions = """\
1111
You are an AI agent responsible for processing orders for food or other items.
1212
Your primary goals are to identify user intent, extract or request provider information, manage order data using tools and friendly responses to guide users through the ordering process.
1313
"""
14+
options = { modelid = "gpt-4o-mini" }
15+
# 👇 alternative syntax to specify options
16+
# [ai.agents.orders.options]
17+
# modelid = "gpt-4o-mini"
1418

1519
# ai.clients.openai, can omit the ai.clients prefix
1620
client = "openai"
17-
use = ["tone", "get_date", "create_order", "cancel_order"]
21+
use = ["tone"]
22+
tools = ["get_date", "create_order", "cancel_order"]
1823

19-
[ai.agents.orders.options]
20-
modelid = "gpt-4o-mini"
21-
# additional properties could be added here
22-
23-
[ai.agents.notes]
24-
description = "Help users create, manage, and retrieve notes effectively."
25-
instructions = """
26-
You are an AI agent that assists users in creating, managing, and retrieving notes.
27-
Your primary goals are to understand user requests related to notes, provide clear and concise responses, and utilize tools to organize and access note data efficiently.
28-
"""
29-
client = "grok"
30-
use = ["tone", "save_notes", "get_date"]
3124

3225
[ai.context.tone]
3326
instructions = """\
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
---
22
id: ai.agents.notes
33
description: Provides free-form memory
4-
client: Grok
5-
options:
6-
modelid: grok-4-fast
4+
client: grok
5+
model: grok-4-fast
6+
use: ["tone"]
7+
tools: ["save_notes", "get_date"]
78
---
89
You organize and keep notes for the user, using JSON-LD

sample/ServiceDefaults.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public static TBuilder ConfigureReload<TBuilder>(this TBuilder builder)
9393
builder.Configuration.AddJsonFile(json, optional: false, reloadOnChange: true);
9494

9595
foreach (var md in Directory.EnumerateFiles(AppContext.BaseDirectory, "*.md", SearchOption.AllDirectories))
96-
builder.Configuration.AddInstructionsFile(md, optional: false, reloadOnChange: true);
96+
builder.Configuration.AddAgentMarkdown(md, optional: false, reloadOnChange: true);
9797
}
9898
else
9999
{
@@ -111,7 +111,7 @@ public static TBuilder ConfigureReload<TBuilder>(this TBuilder builder)
111111
builder.Configuration.AddJsonFile(json, optional: false, reloadOnChange: true);
112112

113113
foreach (var md in Directory.EnumerateFiles(baseDir, "*.md", SearchOption.AllDirectories).Where(IsSource))
114-
builder.Configuration.AddInstructionsFile(md, optional: false, reloadOnChange: true);
114+
builder.Configuration.AddAgentMarkdown(md, optional: false, reloadOnChange: true);
115115
}
116116

117117
return builder;

src/Agents/ConfigurableInstructionsExtensions.cs renamed to src/Agents/AgentMarkdownExtensions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
namespace Devlooped.Agents.AI;
77

88
[EditorBrowsable(EditorBrowsableState.Never)]
9-
public static class ConfigurableInstructionsExtensions
9+
public static class AgentMarkdownExtensions
1010
{
1111
/// <summary>
1212
/// Adds an instructions markdown file with optional YAML front-matter to the configuration sources.
1313
/// </summary>
14-
public static IConfigurationBuilder AddInstructionsFile(this IConfigurationBuilder builder, string path, bool optional = false, bool reloadOnChange = false)
14+
public static IConfigurationBuilder AddAgentMarkdown(this IConfigurationBuilder builder, string path, bool optional = false, bool reloadOnChange = false)
1515
=> builder.Add<InstructionsConfigurationSource>(source =>
1616
{
1717
source.Path = path;
@@ -23,7 +23,7 @@ public static IConfigurationBuilder AddInstructionsFile(this IConfigurationBuild
2323
/// <summary>
2424
/// Adds an instructions markdown stream with optional YAML front-matter to the configuration sources.
2525
/// </summary>
26-
public static IConfigurationBuilder AddInstructionsStream(this IConfigurationBuilder builder, Stream stream)
26+
public static IConfigurationBuilder AddAgentMarkdown(this IConfigurationBuilder builder, Stream stream)
2727
=> Throw.IfNull(builder).Add((InstructionsStreamConfigurationSource source) => source.Stream = stream);
2828

2929
static class InstructionsParser

src/Agents/ConfigurableAIAgent.cs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnum
105105

106106
if (chat is not null)
107107
options.ChatOptions = chat;
108+
else if (options.Model is not null)
109+
(options.ChatOptions ??= new()).ModelId = options.Model;
108110

109111
configure?.Invoke(name, options);
110112

@@ -127,10 +129,10 @@ public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnum
127129

128130
options.AIContextProviderFactory = _ => contextProvider;
129131
}
130-
else if (options.Use?.Count > 0)
132+
else if (options.Use?.Count > 0 || options.Tools?.Count > 0)
131133
{
132134
var contexts = new List<AIContext>();
133-
foreach (var use in options.Use)
135+
foreach (var use in options.Use ?? [])
134136
{
135137
var context = services.GetKeyedService<AIContext>(use);
136138
if (context is not null)
@@ -139,13 +141,6 @@ public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnum
139141
continue;
140142
}
141143

142-
var function = services.GetKeyedService<AITool>(use) ?? services.GetKeyedService<AIFunction>(use);
143-
if (function is not null)
144-
{
145-
contexts.Add(new AIContext { Tools = [function] });
146-
continue;
147-
}
148-
149144
if (configuration.GetSection("ai:context:" + use) is { } ctxSection &&
150145
ctxSection.Get<AIContextConfiguration>() is { } ctxConfig)
151146
{
@@ -161,7 +156,7 @@ public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnum
161156
{
162157
var tool = services.GetKeyedService<AITool>(toolName) ??
163158
services.GetKeyedService<AIFunction>(toolName) ??
164-
throw new InvalidOperationException($"Specified tool '{toolName}' for AI context '{ctxSection.Path}:tools' is not registered, and is required by agent section '{configSection.Path}'.");
159+
throw new InvalidOperationException($"Specified tool '{toolName}' for AI context '{ctxSection.Path}:tools' is not registered as a keyed {nameof(AITool)} or {nameof(AIFunction)}, and is required by agent section '{configSection.Path}'.");
165160

166161
configured.Tools ??= [];
167162
configured.Tools.Add(tool);
@@ -172,7 +167,16 @@ public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnum
172167
continue;
173168
}
174169

175-
throw new InvalidOperationException($"Specified AI context '{use}' for agent '{name}' is not registered as either {nameof(AIContent)}, {nameof(AITool)} or configuration section 'ai:context:{use}'.");
170+
throw new InvalidOperationException($"Specified AI context '{use}' for agent '{name}' is not registered as either {nameof(AIContent)} or configuration section 'ai:context:{use}'.");
171+
}
172+
173+
foreach (var toolName in options.Tools ?? [])
174+
{
175+
var tool = services.GetKeyedService<AITool>(toolName) ??
176+
services.GetKeyedService<AIFunction>(toolName) ??
177+
throw new InvalidOperationException($"Specified tool '{toolName}' for agent '{section}' is not registered as a keyed {nameof(AITool)} or {nameof(AIFunction)}.");
178+
179+
contexts.Add(new AIContext { Tools = [tool] });
176180
}
177181

178182
options.AIContextProviderFactory = _ => new CompositeAIContextProvider(contexts);
@@ -215,7 +219,9 @@ void OnReload(object? state)
215219
internal class AgentClientOptions : ChatClientAgentOptions
216220
{
217221
public string? Client { get; set; }
222+
public string? Model { get; set; }
218223
public IList<string>? Use { get; set; }
224+
public IList<string>? Tools { get; set; }
219225
}
220226
}
221227

src/Tests/ConfigurableAgentTests.cs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ public async Task UseAIToolFromKeyedServiceAsync()
539539
[ai.agents.chat]
540540
description = "Chat agent."
541541
client = "openai"
542-
use = ["get_date"]
542+
tools = ["get_date"]
543543
"""");
544544

545545
AITool tool = AIFunctionFactory.Create(() => DateTimeOffset.Now, "get_date");
@@ -561,6 +561,32 @@ public async Task UseAIToolFromKeyedServiceAsync()
561561
Assert.Same(tool, context.Tools[0]);
562562
}
563563

564+
[Fact]
565+
public async Task MissingAIToolFromKeyedServiceThrows()
566+
{
567+
var builder = new HostApplicationBuilder();
568+
569+
builder.Configuration.AddToml(
570+
$$"""
571+
[ai.clients.openai]
572+
modelid = "gpt-4.1"
573+
apikey = "sk-asdf"
574+
575+
[ai.agents.chat]
576+
description = "Chat agent."
577+
client = "openai"
578+
tools = ["get_date"]
579+
""");
580+
581+
builder.AddAIAgents();
582+
var app = builder.Build();
583+
584+
var exception = Assert.ThrowsAny<Exception>(() => app.Services.GetRequiredKeyedService<AIAgent>("chat"));
585+
586+
Assert.Contains("get_date", exception.Message);
587+
Assert.Contains("ai:agents:chat", exception.Message);
588+
}
589+
564590
[Fact]
565591
public async Task UseAIContextFromSection()
566592
{
@@ -668,5 +694,57 @@ public async Task UnknownUseThrows()
668694

669695
Assert.Contains("foo", exception.Message);
670696
}
697+
698+
[Fact]
699+
public async Task OverrideModelFromAgentChatOptions()
700+
{
701+
var builder = new HostApplicationBuilder();
702+
703+
builder.Configuration.AddToml(
704+
$$"""
705+
[ai.clients.openai]
706+
modelid = "gpt-4.1"
707+
apikey = "sk-asdf"
708+
709+
[ai.agents.chat]
710+
description = "Chat"
711+
client = "openai"
712+
options = { modelid = "gpt-5" }
713+
""");
714+
715+
builder.AddAIAgents();
716+
var app = builder.Build();
717+
718+
var agent = app.Services.GetRequiredKeyedService<AIAgent>("chat");
719+
var options = agent.GetService<ChatClientAgentOptions>();
720+
721+
Assert.Equal("gpt-5", options?.ChatOptions?.ModelId);
722+
}
723+
724+
[Fact]
725+
public async Task OverrideModelFromAgentModel()
726+
{
727+
var builder = new HostApplicationBuilder();
728+
729+
builder.Configuration.AddToml(
730+
$$"""
731+
[ai.clients.openai]
732+
modelid = "gpt-4.1"
733+
apikey = "sk-asdf"
734+
735+
[ai.agents.chat]
736+
description = "Chat"
737+
client = "openai"
738+
model = "gpt-5"
739+
""");
740+
741+
builder.AddAIAgents();
742+
var app = builder.Build();
743+
744+
var agent = app.Services.GetRequiredKeyedService<AIAgent>("chat");
745+
var options = agent.GetService<ChatClientAgentOptions>();
746+
747+
Assert.Equal("gpt-5", options?.ChatOptions?.ModelId);
748+
}
671749
}
672750

0 commit comments

Comments
 (0)