summaryrefslogtreecommitdiff
path: root/src/Agent.Core
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-08-12 00:21:23 +0900
committertanaka takashi <isazaka@gmail.com>2026-08-12 00:21:23 +0900
commit98b6a2aeedd591058c1d81e401d24f3ded162926 (patch)
tree74cbb38af825526e4fc90b150feed14426d727b4 /src/Agent.Core
parentce550a127b7dc972651bd15b9f09979a2c38688f (diff)
fix: implement native Ollama thinking semantics
Model Ollama chat thinking separately from final content, preserve it only across assistant tool-call continuation, and keep private reasoning out of user results, session context, and logs. Accept native boolean and low/medium/high/max think values, normalize YAML booleans, and serialize explicit overrides using the native top-level chat field. Stop forcing think=false for structured planning, skill selection, and context summaries; corrected retries now retain the configured thinking mode. Update the specification, design, audit report, and requirement traceability for OLL-14. Add request/response, configuration, observability, retry, and tool-continuation coverage. Verified with 92 passing Agent.Core tests and a deepseek-v4-flash:cloud think:true MCP operational run.
Diffstat (limited to 'src/Agent.Core')
-rw-r--r--src/Agent.Core/Ollama/OllamaClient.cs5
-rw-r--r--src/Agent.Core/Ollama/OllamaModels.cs7
-rw-r--r--src/Agent.Core/Ollama/OllamaSettings.cs2
-rw-r--r--src/Agent.Core/Runtime/AgentRuntime.cs7
-rw-r--r--src/Agent.Core/Runtime/PlannerService.cs8
5 files changed, 14 insertions, 15 deletions
diff --git a/src/Agent.Core/Ollama/OllamaClient.cs b/src/Agent.Core/Ollama/OllamaClient.cs
index 428bca9..fea5dfb 100644
--- a/src/Agent.Core/Ollama/OllamaClient.cs
+++ b/src/Agent.Core/Ollama/OllamaClient.cs
@@ -57,7 +57,7 @@ public sealed class OllamaClient : IOllamaClient
Model = _settings.Model,
Messages = messages,
Stream = false,
- Think = thinkOverride ?? _settings.Think,
+ Think = thinkOverride is null ? _settings.Think : thinkOverride.Value,
KeepAlive = _settings.KeepAlive,
Options = options,
Tools = tools?.Select(static tool => new OllamaToolDefinition("function", new OllamaFunctionDefinition(tool.Name, tool.Description, tool.Parameters))).ToArray(),
@@ -85,7 +85,8 @@ public sealed class OllamaClient : IOllamaClient
?? throw new OllamaException("Ollama returned an empty response.", false);
if (!string.IsNullOrWhiteSpace(result.Error)) throw new OllamaException(result.Error, false);
if (result.Message is null) throw new OllamaException("Ollama response has no message.", false);
- return new OllamaResult(result.Message.Content, result.PromptEvalCount, result.EvalCount, result.Message.ToolCalls ?? [], result.PromptEvalCount);
+ return new OllamaResult(result.Message.Content, result.PromptEvalCount, result.EvalCount,
+ result.Message.ToolCalls ?? [], result.PromptEvalCount, result.Message.Thinking);
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException && !cancellationToken.IsCancellationRequested)
{
diff --git a/src/Agent.Core/Ollama/OllamaModels.cs b/src/Agent.Core/Ollama/OllamaModels.cs
index f6ff092..9e6103e 100644
--- a/src/Agent.Core/Ollama/OllamaModels.cs
+++ b/src/Agent.Core/Ollama/OllamaModels.cs
@@ -9,7 +9,8 @@ public sealed record OllamaMessage(
[property: JsonPropertyName("content")] string Content,
[property: JsonPropertyName("images"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] IReadOnlyList<string>? Images = null,
[property: JsonPropertyName("tool_name"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? ToolName = null,
- [property: JsonPropertyName("tool_calls"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] IReadOnlyList<OllamaToolCall>? ToolCalls = null);
+ [property: JsonPropertyName("tool_calls"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] IReadOnlyList<OllamaToolCall>? ToolCalls = null,
+ [property: JsonPropertyName("thinking"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? Thinking = null);
public sealed record OllamaToolCall([property: JsonPropertyName("function")] OllamaFunctionCall Function);
public sealed record OllamaFunctionCall(
@@ -29,7 +30,7 @@ public sealed class OllamaChatRequest
[JsonPropertyName("model")] public required string Model { get; init; }
[JsonPropertyName("messages")] public required IReadOnlyList<OllamaMessage> Messages { get; init; }
[JsonPropertyName("stream")] public bool Stream { get; init; }
- [JsonPropertyName("think")] public bool Think { get; init; }
+ [JsonPropertyName("think")] public required object Think { get; init; }
[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; }
@@ -44,4 +45,4 @@ public sealed class OllamaChatResponse
[JsonPropertyName("error")] public string? Error { get; init; }
}
-public sealed record OllamaResult(string Content, long InputTokens, long OutputTokens, IReadOnlyList<OllamaToolCall>? ToolCalls = null, long PromptEvalCount = 0);
+public sealed record OllamaResult(string Content, long InputTokens, long OutputTokens, IReadOnlyList<OllamaToolCall>? ToolCalls = null, long PromptEvalCount = 0, string? Thinking = null);
diff --git a/src/Agent.Core/Ollama/OllamaSettings.cs b/src/Agent.Core/Ollama/OllamaSettings.cs
index 94ab3ef..9f33208 100644
--- a/src/Agent.Core/Ollama/OllamaSettings.cs
+++ b/src/Agent.Core/Ollama/OllamaSettings.cs
@@ -5,6 +5,6 @@ namespace Agent.Core.Ollama;
public sealed record OllamaSettings(
Uri Endpoint,
string Model,
- bool Think,
+ object Think,
string KeepAlive,
IReadOnlyDictionary<string, JsonElement> Options);
diff --git a/src/Agent.Core/Runtime/AgentRuntime.cs b/src/Agent.Core/Runtime/AgentRuntime.cs
index 31a4414..5ca9d1c 100644
--- a/src/Agent.Core/Runtime/AgentRuntime.cs
+++ b/src/Agent.Core/Runtime/AgentRuntime.cs
@@ -262,7 +262,7 @@ public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, Ag
IReadOnlyList<OllamaToolCall> calls = result.ToolCalls ?? [];
if (calls.Count > 0)
{
- messages.Add(new OllamaMessage("assistant", result.Content, ToolCalls: calls));
+ messages.Add(new OllamaMessage("assistant", result.Content, Thinking: result.Thinking, ToolCalls: calls));
string? failureFingerprint = null;
foreach (OllamaToolCall call in calls)
{
@@ -400,7 +400,7 @@ public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, Ag
callContext = new AgentLlmCallContext(Guid.NewGuid(), task.SessionId, taskId, null,
"context_summary", iteration, counters.Replans, settings.Model);
_observer.LlmRequest(callContext, messages);
- OllamaResult result = await ollama.ChatStructuredAsync(messages, task.Temperature, schema.RootElement.Clone(), false, cancellationToken).ConfigureAwait(false);
+ OllamaResult result = await ollama.ChatStructuredAsync(messages, task.Temperature, schema.RootElement.Clone(), null, cancellationToken).ConfigureAwait(false);
_observer.LlmResponse(callContext, result);
registry.AddUsage(taskId, result.InputTokens, result.OutputTokens);
registry.UpdateDiagnostics(taskId, counters.Iterations, counters.Replans);
@@ -444,8 +444,7 @@ public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, Ag
OllamaResult? result = null;
try
{
- bool? thinkOverride = attempt == 0 ? null : false;
- result = await ollama.ChatStructuredAsync(attemptMessages, task.Temperature, formatDoc.RootElement.Clone(), thinkOverride, cancellationToken).ConfigureAwait(false);
+ result = await ollama.ChatStructuredAsync(attemptMessages, task.Temperature, formatDoc.RootElement.Clone(), null, cancellationToken).ConfigureAwait(false);
_observer.LlmResponse(callContext, result);
registry.AddUsage(task.TaskId, result.InputTokens, result.OutputTokens);
registry.UpdateDiagnostics(task.TaskId, counters.Iterations, counters.Replans);
diff --git a/src/Agent.Core/Runtime/PlannerService.cs b/src/Agent.Core/Runtime/PlannerService.cs
index bc96ec7..3591259 100644
--- a/src/Agent.Core/Runtime/PlannerService.cs
+++ b/src/Agent.Core/Runtime/PlannerService.cs
@@ -218,8 +218,7 @@ public sealed class PlannerService(IOllamaClient ollama, AgentRuntimeSettings se
OllamaResult? result = null;
try
{
- bool? thinkOverride = attempt == 0 ? null : false;
- result = await _ollama.ChatStructuredAsync(attemptMessages, temperature, formatDoc.RootElement.Clone(), thinkOverride, cancellationToken).ConfigureAwait(false);
+ result = await _ollama.ChatStructuredAsync(attemptMessages, temperature, formatDoc.RootElement.Clone(), null, cancellationToken).ConfigureAwait(false);
if (!TryParseSelectedSkills(result.Content, out IReadOnlyList<AgentSkill> selected))
throw new PlannerValidationException("Ollama returned an invalid skill selection object.");
observeAttempt?.Invoke("skill_selection", attemptMessages, result, null);
@@ -250,8 +249,7 @@ public sealed class PlannerService(IOllamaClient ollama, AgentRuntimeSettings se
OllamaResult? result = null;
try
{
- bool? thinkOverride = attempt == 0 ? null : false;
- result = await _ollama.ChatStructuredAsync(attemptMessages, temperature, PlanSchema.Element, thinkOverride, cancellationToken).ConfigureAwait(false);
+ result = await _ollama.ChatStructuredAsync(attemptMessages, temperature, PlanSchema.Element, null, cancellationToken).ConfigureAwait(false);
PlanResult? plan = ParsePlan(result.Content);
if (plan is null || plan.Tasks.Count == 0)
throw new PlannerValidationException("Ollama returned an empty or invalid structured plan.");
@@ -274,7 +272,7 @@ public sealed class PlannerService(IOllamaClient ollama, AgentRuntimeSettings se
}
}
}
- throw new PlannerValidationException($"Structured {phase} failed after think=false retry: {lastFailure?.Message}");
+ throw new PlannerValidationException($"Structured {phase} failed after one corrected retry: {lastFailure?.Message}");
}
private bool TryParseSelectedSkills(string content, out IReadOnlyList<AgentSkill> selected)