feat: add structured prompt invocation backend

- parse prompt tools metadata and carry structured promptInvocation payloads
- store prompt invocation metadata on trigger messages for replay-safe reruns
- route prompt agents through per-turn plan or Copilot agent overrides
- restrict prompt-scoped tools in sidecar session configuration
- auto-rescan project customization files with debounced watchers
- document the new customization watcher and prompt invocation flow

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-02 11:41:18 +02:00
co-authored by Copilot
parent aa7830f01a
commit 5d69d9d855
21 changed files with 900 additions and 47 deletions
@@ -184,6 +184,48 @@ public sealed class AgentInstructionComposerTests
< instructions.IndexOf("scratchpad mode", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void Compose_AppendsPromptInvocationAsATaskDirective()
{
PatternDefinitionDto pattern = new()
{
Id = "pattern-single",
Name = "Single",
Mode = "single",
Availability = "available",
};
PatternAgentDefinitionDto agent = CreateAgent(
id: "agent-primary",
name: "Primary Agent",
instructions: "You are a helpful assistant.");
string instructions = AgentInstructionComposer.Compose(
pattern,
agent,
agentIndex: 0,
promptInvocation: new RunTurnPromptInvocationDto
{
Id = "project_customization_prompt_doc_review",
Name = "doc-review",
SourcePath = @".github\prompts\docs\doc-review.prompt.md",
Description = "Review docs for missing steps",
Agent = "plan",
Tools = ["view", "glob"],
ResolvedPrompt = "Review the docs for missing steps and propose updates."
});
Assert.Contains("repository prompt file", instructions, StringComparison.OrdinalIgnoreCase);
Assert.Contains(@"Source: .github\prompts\docs\doc-review.prompt.md", instructions, StringComparison.Ordinal);
Assert.Contains("Name: doc-review", instructions, StringComparison.Ordinal);
Assert.Contains("Description: Review docs for missing steps", instructions, StringComparison.Ordinal);
Assert.Contains("Agent: plan", instructions, StringComparison.Ordinal);
Assert.Contains("Tools: view, glob", instructions, StringComparison.Ordinal);
Assert.Contains(
"Prompt instructions:\nReview the docs for missing steps and propose updates.",
instructions,
StringComparison.Ordinal);
}
private static PatternAgentDefinitionDto CreateAgent(string id, string name, string instructions)
{
return new PatternAgentDefinitionDto
@@ -54,6 +54,34 @@ public sealed class CopilotAgentBundleTests
Assert.Equal(["glob", "view"], sessionConfig.AvailableTools);
}
[Fact]
public void ApplyPromptInvocation_RestrictsAvailableToolsAndKeepsHandoffTools()
{
SessionConfig sessionConfig = new()
{
AvailableTools = ["view", "glob", "edit"],
Tools = [CreateTool("view"), CreateTool("edit"), CreateTool("handoff_to_reviewer")],
};
CopilotAgentBundle.ApplyPromptInvocation(
sessionConfig,
new RunTurnPromptInvocationDto
{
Id = "project_customization_prompt_doc_review",
Name = "doc-review",
SourcePath = @".github\prompts\docs\doc-review.prompt.md",
ResolvedPrompt = "Review the docs for missing steps.",
Tools = ["view"],
});
Assert.Equal(["view", "ask_user", "report_intent", "task_complete"], sessionConfig.AvailableTools);
AIFunction[] tools = Assert.IsAssignableFrom<IEnumerable<AIFunction>>(sessionConfig.Tools).ToArray();
Assert.Equal(2, tools.Length);
Assert.Contains(tools, tool => tool.Name == "view");
Assert.Contains(tools, tool => tool.Name == "handoff_to_reviewer");
}
[Fact]
public void Constructor_StoresWhetherHooksAreConfigured()
{
@@ -426,6 +454,95 @@ public sealed class CopilotAgentBundleTests
Assert.Equal("Help.\n\nFollow repository guidance.", sessionConfig.SystemMessage?.Content);
}
[Fact]
public void CreateSessionConfig_UsesPromptAgentOverride()
{
RunTurnCommandDto command = new()
{
SessionId = "session-1",
ProjectPath = @"C:\workspace\project",
WorkspaceKind = "project",
Mode = "interactive",
PromptInvocation = new RunTurnPromptInvocationDto
{
Id = "project_customization_prompt_doc_review",
Name = "doc-review",
SourcePath = @".github\prompts\docs\doc-review.prompt.md",
Agent = "designer",
ResolvedPrompt = "Review the docs for missing steps.",
},
Pattern = new PatternDefinitionDto
{
Id = "pattern-1",
Name = "Pattern",
Mode = "single",
Availability = "available",
Agents =
[
new PatternAgentDefinitionDto
{
Id = "agent-1",
Name = "Primary",
Model = "gpt-5.4",
Instructions = "Help.",
},
],
},
};
SessionConfig sessionConfig = CopilotAgentBundle.CreateSessionConfig(
command,
command.Pattern.Agents[0],
agentIndex: 0);
Assert.Equal("designer", sessionConfig.Agent);
Assert.Contains("Review the docs for missing steps.", sessionConfig.SystemMessage?.Content, StringComparison.Ordinal);
}
[Fact]
public void CreateSessionConfig_DefaultsPromptToolInvocationsToAgentMode()
{
RunTurnCommandDto command = new()
{
SessionId = "session-1",
ProjectPath = @"C:\workspace\project",
WorkspaceKind = "project",
Mode = "interactive",
PromptInvocation = new RunTurnPromptInvocationDto
{
Id = "project_customization_prompt_doc_review",
Name = "doc-review",
SourcePath = @".github\prompts\docs\doc-review.prompt.md",
ResolvedPrompt = "Review the docs for missing steps.",
Tools = ["view"],
},
Pattern = new PatternDefinitionDto
{
Id = "pattern-1",
Name = "Pattern",
Mode = "single",
Availability = "available",
Agents =
[
new PatternAgentDefinitionDto
{
Id = "agent-1",
Name = "Primary",
Model = "gpt-5.4",
Instructions = "Help.",
},
],
},
};
SessionConfig sessionConfig = CopilotAgentBundle.CreateSessionConfig(
command,
command.Pattern.Agents[0],
agentIndex: 0);
Assert.Equal("agent", sessionConfig.Agent);
}
[Fact]
public async Task CopilotSessionHooks_Create_UsesApprovalPolicyForPreToolUse()
{
@@ -484,7 +601,7 @@ public sealed class CopilotAgentBundleTests
Assert.Equal("agent-ux", agentId);
}
private static AIFunction CreateTool()
private static AIFunction CreateTool(string name = "echo")
{
ToolTarget target = new();
MethodInfo method = typeof(ToolTarget).GetMethod(nameof(ToolTarget.Echo))
@@ -495,7 +612,7 @@ public sealed class CopilotAgentBundleTests
target,
new AIFunctionFactoryOptions
{
Name = "echo",
Name = name,
Description = "Echo test tool",
});
}