summaryrefslogtreecommitdiff
path: root/src/Agent.Core
diff options
context:
space:
mode:
Diffstat (limited to 'src/Agent.Core')
-rw-r--r--src/Agent.Core/Ollama/IOllamaClient.cs3
-rw-r--r--src/Agent.Core/Ollama/OllamaClient.cs12
-rw-r--r--src/Agent.Core/Ollama/OllamaModels.cs1
-rw-r--r--src/Agent.Core/Runtime/AgentRuntime.cs67
4 files changed, 78 insertions, 5 deletions
diff --git a/src/Agent.Core/Ollama/IOllamaClient.cs b/src/Agent.Core/Ollama/IOllamaClient.cs
index a03a353..d1cb99f 100644
--- a/src/Agent.Core/Ollama/IOllamaClient.cs
+++ b/src/Agent.Core/Ollama/IOllamaClient.cs
@@ -1,5 +1,6 @@
namespace Agent.Core.Ollama;
+using System.Text.Json;
using Agent.Core.Tools;
public interface IOllamaClient
@@ -7,5 +8,7 @@ public interface IOllamaClient
Task<OllamaResult> ChatAsync(IReadOnlyList<OllamaMessage> messages, double? temperatureOverride, CancellationToken cancellationToken);
Task<OllamaResult> ChatWithToolsAsync(IReadOnlyList<OllamaMessage> messages, double? temperatureOverride, IReadOnlyList<InternalToolDefinition> tools, CancellationToken cancellationToken) =>
ChatAsync(messages, temperatureOverride, cancellationToken);
+ Task<OllamaResult> ChatStructuredAsync(IReadOnlyList<OllamaMessage> messages, double? temperatureOverride, JsonElement format, CancellationToken cancellationToken) =>
+ ChatAsync(messages, temperatureOverride, cancellationToken);
Task<bool?> SupportsVisionAsync(CancellationToken cancellationToken) => Task.FromResult<bool?>(null);
}
diff --git a/src/Agent.Core/Ollama/OllamaClient.cs b/src/Agent.Core/Ollama/OllamaClient.cs
index bfd2210..c761081 100644
--- a/src/Agent.Core/Ollama/OllamaClient.cs
+++ b/src/Agent.Core/Ollama/OllamaClient.cs
@@ -22,10 +22,13 @@ public sealed class OllamaClient : IOllamaClient
}
public async Task<OllamaResult> ChatAsync(IReadOnlyList<OllamaMessage> messages, double? temperatureOverride, CancellationToken cancellationToken)
- => await ChatCoreAsync(messages, temperatureOverride, null, cancellationToken).ConfigureAwait(false);
+ => await ChatCoreAsync(messages, temperatureOverride, null, null, cancellationToken).ConfigureAwait(false);
public async Task<OllamaResult> ChatWithToolsAsync(IReadOnlyList<OllamaMessage> messages, double? temperatureOverride, IReadOnlyList<InternalToolDefinition> tools, CancellationToken cancellationToken)
- => await ChatCoreAsync(messages, temperatureOverride, tools, cancellationToken).ConfigureAwait(false);
+ => await ChatCoreAsync(messages, temperatureOverride, tools, null, cancellationToken).ConfigureAwait(false);
+
+ public async Task<OllamaResult> ChatStructuredAsync(IReadOnlyList<OllamaMessage> messages, double? temperatureOverride, JsonElement format, CancellationToken cancellationToken)
+ => await ChatCoreAsync(messages, temperatureOverride, null, format, cancellationToken).ConfigureAwait(false);
public async Task<bool?> SupportsVisionAsync(CancellationToken cancellationToken)
{
@@ -42,7 +45,7 @@ public sealed class OllamaClient : IOllamaClient
catch (HttpRequestException) { return null; }
}
- private async Task<OllamaResult> ChatCoreAsync(IReadOnlyList<OllamaMessage> messages, double? temperatureOverride, IReadOnlyList<InternalToolDefinition>? tools, CancellationToken cancellationToken)
+ private async Task<OllamaResult> ChatCoreAsync(IReadOnlyList<OllamaMessage> messages, double? temperatureOverride, IReadOnlyList<InternalToolDefinition>? tools, JsonElement? format, CancellationToken cancellationToken)
{
Dictionary<string, JsonElement> options = _settings.Options.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal);
if (temperatureOverride is not null) options["temperature"] = JsonSerializer.SerializeToElement(temperatureOverride.Value);
@@ -54,7 +57,8 @@ public sealed class OllamaClient : IOllamaClient
Think = _settings.Think,
KeepAlive = _settings.KeepAlive,
Options = options,
- Tools = tools?.Select(static tool => new OllamaToolDefinition("function", new OllamaFunctionDefinition(tool.Name, tool.Description, tool.Parameters))).ToArray()
+ Tools = tools?.Select(static tool => new OllamaToolDefinition("function", new OllamaFunctionDefinition(tool.Name, tool.Description, tool.Parameters))).ToArray(),
+ Format = format
};
for (int attempt = 0; ; attempt++)
diff --git a/src/Agent.Core/Ollama/OllamaModels.cs b/src/Agent.Core/Ollama/OllamaModels.cs
index 823a178..f4b3884 100644
--- a/src/Agent.Core/Ollama/OllamaModels.cs
+++ b/src/Agent.Core/Ollama/OllamaModels.cs
@@ -33,6 +33,7 @@ public sealed class OllamaChatRequest
[JsonPropertyName("keep_alive")] public required string KeepAlive { get; init; }
[JsonPropertyName("options")] public required IReadOnlyDictionary<string, System.Text.Json.JsonElement> Options { get; init; }
[JsonPropertyName("tools"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IReadOnlyList<OllamaToolDefinition>? Tools { get; init; }
+ [JsonPropertyName("format"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public JsonElement? Format { get; init; }
}
public sealed class OllamaChatResponse
diff --git a/src/Agent.Core/Runtime/AgentRuntime.cs b/src/Agent.Core/Runtime/AgentRuntime.cs
index 228ac41..f6bb483 100644
--- a/src/Agent.Core/Runtime/AgentRuntime.cs
+++ b/src/Agent.Core/Runtime/AgentRuntime.cs
@@ -1,3 +1,4 @@
+using System.Text.Json;
using Agent.Core.Ollama;
using Agent.Core.Prompts;
using Agent.Core.Sessions;
@@ -28,12 +29,16 @@ public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, Ag
await _tools.BeginTaskAsync(taskId, cancellationToken).ConfigureAwait(false);
if (task.Images.Count > 0 && await ollama.SupportsVisionAsync(cancellationToken).ConfigureAwait(false) is false)
throw new InvalidOperationException(settings.VisionUnsupportedMessage);
+
+ // Progressive Disclosure Stage 2: LLM Skill Selection.
+ IReadOnlyList<AgentSkill> selectedSkills = await SelectSkillsAsync(task.Request, cancellationToken).ConfigureAwait(false);
+
var messages = PromptComposer.Compose(new PromptContext(
FixedPrompt.Load(),
settings.UserSystemPrompt,
settings.Locale,
settings.Skills,
- [], // SelectedSkills: populated after LLM skill selection (Progressive Disclosure Stage 2→3).
+ selectedSkills, // Stage 3: selected skill instructions injected by PromptComposer.
settings.SandboxCapabilitySummary,
"Current task: " + task.Request,
string.Empty,
@@ -134,6 +139,66 @@ public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, Ag
}
}
+ private async Task<IReadOnlyList<AgentSkill>> SelectSkillsAsync(string request, CancellationToken cancellationToken)
+ {
+ if (settings.Skills.Count == 0) return [];
+
+ // Build a minimal prompt with skill catalog for the LLM to choose from.
+ string catalog = string.Join("\n", settings.Skills.Select(static s => $"- {s.Name}: {s.Description}"));
+ var selectionMessages = new List<OllamaMessage>
+ {
+ new("system", $"Select skills relevant to the user request. Available skills:\n{catalog}\n\nReturn a JSON object with a 'selected_skills' array containing skill names. Select only relevant skills. If none are relevant, return an empty array."),
+ new("user", request)
+ };
+
+ // JSON Schema for structured output.
+ string schema = """{"type":"object","properties":{"selected_skills":{"type":"array","items":{"type":"string"}}},"required":["selected_skills"]}""";
+ using JsonDocument formatDoc = JsonDocument.Parse(schema);
+
+ OllamaResult result;
+ try
+ {
+ result = await ollama.ChatStructuredAsync(selectionMessages, 0.0, formatDoc.RootElement.Clone(), cancellationToken).ConfigureAwait(false);
+ }
+ catch
+ {
+ // If structured output fails (e.g., model doesn't support it), fall back to no skills.
+ return [];
+ }
+
+ return ParseSelectedSkills(result.Content);
+ }
+
+ private IReadOnlyList<AgentSkill> ParseSelectedSkills(string content)
+ {
+ try
+ {
+ using JsonDocument doc = JsonDocument.Parse(content);
+ if (!doc.RootElement.TryGetProperty("selected_skills", out JsonElement skillsElement)) return [];
+ var selectedNames = new HashSet<string>(StringComparer.Ordinal);
+ foreach (JsonElement item in skillsElement.EnumerateArray())
+ {
+ string? name = item.GetString();
+ if (!string.IsNullOrWhiteSpace(name)) selectedNames.Add(name);
+ }
+ // Validate: only known skills, no duplicates, max 10.
+ var valid = new List<AgentSkill>();
+ foreach (AgentSkill skill in settings.Skills)
+ {
+ if (selectedNames.Contains(skill.Name))
+ {
+ valid.Add(skill);
+ if (valid.Count >= 10) break;
+ }
+ }
+ return valid;
+ }
+ catch
+ {
+ return [];
+ }
+ }
+
private async Task<string?> EndTaskSafelyAsync(Guid taskId, bool success, CancellationToken cancellationToken)
{
try