Merge claudedo/123b0241b5e94b69bfa592fde89c51aa
This commit is contained in:
+66
-1
@@ -1,7 +1,10 @@
|
||||
using System.ComponentModel;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker.Hub;
|
||||
using ClaudeDo.Worker.Runner;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ClaudeDo.Worker.External;
|
||||
@@ -11,18 +14,36 @@ public sealed record TaskConfigResult(bool Found, TaskConfigDto? Config);
|
||||
public sealed record SetListConfigResult(bool Ok, string ListId, TaskConfigDto? Config);
|
||||
public sealed record SetTaskConfigResult(bool Ok, string TaskId, TaskConfigDto? Config);
|
||||
|
||||
public sealed record EffectiveModelDto(string Value, string Source);
|
||||
public sealed record EffectiveMaxTurnsDto(int Effective, string Source, int Requested, bool Clamped);
|
||||
public sealed record EffectiveAgentPathDto(string? Value, string? Source);
|
||||
public sealed record EffectiveSystemPromptDto(bool Set, IReadOnlyList<string> Sources);
|
||||
public sealed record EffectiveRunConfigDto(
|
||||
string TaskId,
|
||||
EffectiveModelDto Model,
|
||||
EffectiveMaxTurnsDto MaxTurns,
|
||||
string Effort,
|
||||
string PermissionMode,
|
||||
EffectiveAgentPathDto AgentPath,
|
||||
EffectiveSystemPromptDto SystemPrompt,
|
||||
IReadOnlyList<string> SkillNames);
|
||||
|
||||
[McpServerToolType]
|
||||
public sealed class ConfigMcpTools
|
||||
{
|
||||
private readonly ListRepository _lists;
|
||||
private readonly TaskRepository _tasks;
|
||||
private readonly HubBroadcaster _broadcaster;
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
|
||||
public ConfigMcpTools(ListRepository lists, TaskRepository tasks, HubBroadcaster broadcaster)
|
||||
public ConfigMcpTools(
|
||||
ListRepository lists, TaskRepository tasks, HubBroadcaster broadcaster,
|
||||
IDbContextFactory<ClaudeDoDbContext> dbFactory)
|
||||
{
|
||||
_lists = lists;
|
||||
_tasks = tasks;
|
||||
_broadcaster = broadcaster;
|
||||
_dbFactory = dbFactory;
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Get a list's default config (model, system prompt, agent path). Returns { found: false, config: null } if no config is set.")]
|
||||
@@ -97,4 +118,48 @@ public sealed class ConfigMcpTools
|
||||
return new TaskConfigResult(false, null);
|
||||
return new TaskConfigResult(true, new TaskConfigDto(task.Model, task.SystemPrompt, task.AgentPath, task.MaxTurns));
|
||||
}
|
||||
|
||||
[McpServerTool, Description(
|
||||
"Get the config a task will ACTUALLY run with — model, max turns, effort, permission mode, agent path, " +
|
||||
"whether a system prompt is set, and skill names — with each field's source (task/list/preset/global). " +
|
||||
"Uses the exact same resolution TaskRunner runs with, so this never drifts from get_app_settings/" +
|
||||
"get_task_config's raw, possibly-unused values. maxTurns also reports the raw requested value and " +
|
||||
"whether it was clamped to the global ceiling. Read-only, no side effects.")]
|
||||
public async Task<EffectiveRunConfigDto> GetEffectiveRunConfig(string taskId, CancellationToken cancellationToken)
|
||||
{
|
||||
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||
var listConfig = await _lists.GetConfigAsync(task.ListId, cancellationToken);
|
||||
|
||||
AppSettingsEntity global;
|
||||
using (var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken))
|
||||
global = await new AppSettingsRepository(ctx).GetAsync(cancellationToken);
|
||||
|
||||
var systemFile = PromptFiles.ReadOrDefault(PromptKind.System);
|
||||
var isImprovementChild = task.ParentTaskId is not null && task.CreatedBy == task.ParentTaskId;
|
||||
var improvementPrompt = isImprovementChild ? PromptFiles.ReadOrDefault(PromptKind.ImprovementChild) : null;
|
||||
|
||||
var effective = EffectiveRunConfigResolver.Resolve(task, listConfig, global, systemFile, improvementPrompt);
|
||||
|
||||
var requestedSkills = TaskRunner.UnionSkillNames(task.SessionSkills, listConfig?.SessionSkills, global.SessionSkills);
|
||||
var skillNames = requestedSkills;
|
||||
if (requestedSkills.Count > 0)
|
||||
{
|
||||
List<SessionSkillEntity> installed;
|
||||
using (var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken))
|
||||
installed = (await new SessionSkillRepository(ctx).ListAsync(cancellationToken)).ToList();
|
||||
var installedNames = installed.Select(s => s.Name).ToHashSet(StringComparer.Ordinal);
|
||||
skillNames = TaskRunner.FilterToInstalled(requestedSkills, installedNames);
|
||||
}
|
||||
|
||||
return new EffectiveRunConfigDto(
|
||||
taskId,
|
||||
new EffectiveModelDto(effective.Model, effective.ModelSource),
|
||||
new EffectiveMaxTurnsDto(effective.MaxTurns, effective.MaxTurnsSource, effective.RequestedMaxTurns, effective.MaxTurnsClamped),
|
||||
effective.Effort,
|
||||
effective.PermissionMode,
|
||||
new EffectiveAgentPathDto(effective.AgentPath, effective.AgentPathSource),
|
||||
new EffectiveSystemPromptDto(effective.SystemPromptSet, effective.SystemPromptSources),
|
||||
skillNames);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using ClaudeDo.Data.Models;
|
||||
|
||||
namespace ClaudeDo.Worker.Runner;
|
||||
|
||||
/// <summary>The task/list/preset/global resolution <see cref="TaskRunner"/> actually runs with,
|
||||
/// plus where each value came from — so a caller can tell a genuine default from an override
|
||||
/// without re-deriving the resolution order itself.</summary>
|
||||
public sealed record EffectiveRunConfig(
|
||||
string Model, string ModelSource,
|
||||
int MaxTurns, string MaxTurnsSource, int RequestedMaxTurns, bool MaxTurnsClamped,
|
||||
string Effort,
|
||||
string? AgentPath, string? AgentPathSource,
|
||||
string PermissionMode,
|
||||
bool SystemPromptSet, IReadOnlyList<string> SystemPromptSources);
|
||||
|
||||
/// <summary>Single source of truth for "which value wins" — shared by <see cref="TaskRunner"/>'s
|
||||
/// actual run path and any read-only reporting of the same resolution (e.g. the
|
||||
/// <c>get_effective_run_config</c> MCP tool), so the two can never drift apart.</summary>
|
||||
public static class EffectiveRunConfigResolver
|
||||
{
|
||||
public static EffectiveRunConfig Resolve(
|
||||
TaskEntity task, ListConfigEntity? listConfig, AppSettingsEntity global,
|
||||
string? systemFile, string? improvementPrompt)
|
||||
{
|
||||
var model = task.Model ?? listConfig?.Model ?? global.DefaultModel;
|
||||
var modelSource = task.Model is not null ? "task" : listConfig?.Model is not null ? "list" : "global";
|
||||
|
||||
var preset = ModelPresets.For(global.ModelPresets, model, global.DefaultMaxTurns);
|
||||
|
||||
var requestedMaxTurns = task.MaxTurns ?? listConfig?.MaxTurns ?? preset.MaxTurns;
|
||||
var maxTurnsSource = task.MaxTurns is not null ? "task" : listConfig?.MaxTurns is not null ? "list" : "preset";
|
||||
// ResolveMaxTurns is declared int? but always returns a value (Math.Min of non-null inputs).
|
||||
var maxTurns = TaskRunner.ResolveMaxTurns(task.MaxTurns, listConfig?.MaxTurns, preset.MaxTurns, global.MaxTurnsCeiling)!.Value;
|
||||
|
||||
var agentPath = task.AgentPath ?? listConfig?.AgentPath;
|
||||
var agentPathSource = task.AgentPath is not null ? "task" : listConfig?.AgentPath is not null ? "list" : null;
|
||||
|
||||
var systemPromptSources = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(systemFile)) systemPromptSources.Add("systemFile");
|
||||
if (!string.IsNullOrWhiteSpace(improvementPrompt)) systemPromptSources.Add("improvementPrompt");
|
||||
if (!string.IsNullOrWhiteSpace(global.DefaultClaudeInstructions)) systemPromptSources.Add("global");
|
||||
if (!string.IsNullOrWhiteSpace(listConfig?.SystemPrompt)) systemPromptSources.Add("list");
|
||||
if (!string.IsNullOrWhiteSpace(task.SystemPrompt)) systemPromptSources.Add("task");
|
||||
|
||||
return new EffectiveRunConfig(
|
||||
model, modelSource,
|
||||
maxTurns, maxTurnsSource, requestedMaxTurns, maxTurns < requestedMaxTurns,
|
||||
preset.Effort,
|
||||
agentPath, agentPathSource,
|
||||
global.DefaultPermissionMode,
|
||||
systemPromptSources.Count > 0, systemPromptSources);
|
||||
}
|
||||
}
|
||||
@@ -574,29 +574,25 @@ public sealed class TaskRunner
|
||||
var requestedSkills = UnionSkillNames(task.SessionSkills, listConfig?.SessionSkills, global.SessionSkills);
|
||||
var skillNames = await FilterToInstalledSkillsAsync(task.Id, requestedSkills, ct);
|
||||
|
||||
// The model decides the global effort/turn defaults: one preset row per model alias
|
||||
// (Settings → General). List- and task-level max-turns overrides still win.
|
||||
var model = task.Model ?? listConfig?.Model ?? global.DefaultModel;
|
||||
var preset = Data.Models.ModelPresets.For(global.ModelPresets, model, global.DefaultMaxTurns);
|
||||
|
||||
var requestedMaxTurns = task.MaxTurns ?? listConfig?.MaxTurns ?? preset.MaxTurns;
|
||||
var maxTurns = ResolveMaxTurns(task.MaxTurns, listConfig?.MaxTurns, preset.MaxTurns, global.MaxTurnsCeiling);
|
||||
if (maxTurns < requestedMaxTurns)
|
||||
// Model/max-turns/effort/agent-path resolution is shared with get_effective_run_config
|
||||
// so the two can never report different numbers for the same task.
|
||||
var effective = EffectiveRunConfigResolver.Resolve(task, listConfig, global, systemFile, improvementPrompt);
|
||||
if (effective.MaxTurnsClamped)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Task {TaskId}: max turns clamped to ceiling (requested={Requested}, effective={Effective}, ceiling={Ceiling})",
|
||||
task.Id, requestedMaxTurns, maxTurns, global.MaxTurnsCeiling);
|
||||
task.Id, effective.RequestedMaxTurns, effective.MaxTurns, global.MaxTurnsCeiling);
|
||||
}
|
||||
|
||||
return new ClaudeRunConfig(
|
||||
Model: model,
|
||||
Model: effective.Model,
|
||||
SystemPrompt: string.IsNullOrWhiteSpace(instructions) ? null : instructions,
|
||||
AgentPath: task.AgentPath ?? listConfig?.AgentPath,
|
||||
AgentPath: effective.AgentPath,
|
||||
ResumeSessionId: resumeSessionId,
|
||||
MaxTurns: maxTurns,
|
||||
PermissionMode: global.DefaultPermissionMode,
|
||||
MaxTurns: effective.MaxTurns,
|
||||
PermissionMode: effective.PermissionMode,
|
||||
SkillNames: skillNames,
|
||||
Effort: preset.Effort);
|
||||
Effort: effective.Effort);
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<string>> FilterToInstalledSkillsAsync(
|
||||
@@ -612,7 +608,7 @@ public sealed class TaskRunner
|
||||
}
|
||||
|
||||
var installedNames = installed.Select(s => s.Name).ToHashSet(StringComparer.Ordinal);
|
||||
var resolved = requestedSkills.Where(installedNames.Contains).ToList();
|
||||
var resolved = FilterToInstalled(requestedSkills, installedNames);
|
||||
var dropped = requestedSkills.Where(n => !installedNames.Contains(n)).ToList();
|
||||
if (dropped.Count > 0)
|
||||
{
|
||||
@@ -624,6 +620,12 @@ public sealed class TaskRunner
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/// <summary>Shared with get_effective_run_config so reported skill names match what a run
|
||||
/// would actually filter down to.</summary>
|
||||
internal static IReadOnlyList<string> FilterToInstalled(
|
||||
IReadOnlyList<string> requestedSkills, IReadOnlySet<string> installedNames)
|
||||
=> requestedSkills.Where(installedNames.Contains).ToList();
|
||||
|
||||
internal static IReadOnlyList<string> UnionSkillNames(params string?[] jsonArrays)
|
||||
{
|
||||
var names = new List<string>();
|
||||
|
||||
Reference in New Issue
Block a user