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
+56
View File
@@ -37,11 +37,22 @@ export interface ProjectPromptFile {
name: string;
description?: string;
agent?: string;
tools?: string[];
template: string;
variables: ProjectPromptVariable[];
sourcePath: string;
}
export interface ProjectPromptInvocation {
id: string;
name: string;
sourcePath: string;
resolvedPrompt: string;
description?: string;
agent?: string;
tools?: string[];
}
export interface ProjectCustomizationState {
instructions: ProjectInstructionFile[];
agentProfiles: ProjectAgentProfile[];
@@ -262,9 +273,54 @@ function normalizeProjectPromptFile(promptFile: ProjectPromptFile): ProjectPromp
normalizedPromptFile.agent = agent;
}
const tools = normalizeOptionalStringArray(promptFile.tools);
if (tools) {
normalizedPromptFile.tools = tools;
}
return normalizedPromptFile;
}
export function normalizeProjectPromptInvocation(
promptInvocation?: ProjectPromptInvocation,
): ProjectPromptInvocation | undefined {
if (!promptInvocation) {
return undefined;
}
const normalizedPromptInvocation: ProjectPromptInvocation = {
id: promptInvocation.id.trim(),
name: promptInvocation.name.trim(),
sourcePath: normalizePathLikeString(promptInvocation.sourcePath),
resolvedPrompt: promptInvocation.resolvedPrompt.trim(),
};
if (
normalizedPromptInvocation.id.length === 0
|| normalizedPromptInvocation.name.length === 0
|| normalizedPromptInvocation.resolvedPrompt.length === 0
) {
return undefined;
}
const description = normalizeOptionalString(promptInvocation.description);
if (description) {
normalizedPromptInvocation.description = description;
}
const agent = normalizeOptionalString(promptInvocation.agent);
if (agent) {
normalizedPromptInvocation.agent = agent;
}
const tools = normalizeOptionalStringArray(promptInvocation.tools);
if (tools) {
normalizedPromptInvocation.tools = tools;
}
return normalizedPromptInvocation;
}
function compareProjectFiles(
left: Pick<ProjectInstructionFile | ProjectAgentProfile | ProjectPromptFile, 'sourcePath' | 'id'>,
right: Pick<ProjectInstructionFile | ProjectAgentProfile | ProjectPromptFile, 'sourcePath' | 'id'>,
+19
View File
@@ -16,6 +16,10 @@ import type { PendingPlanReviewRecord } from '@shared/domain/planReview';
import type { PendingMcpAuthRecord } from '@shared/domain/mcpAuth';
import type { ChatMessageAttachment } from '@shared/domain/attachment';
import type { InteractionMode } from '@shared/contracts/sidecar';
import {
normalizeProjectPromptInvocation,
type ProjectPromptInvocation,
} from '@shared/domain/projectCustomization';
export type ChatRole = 'system' | 'user' | 'assistant';
export type ChatMessageKind = 'response' | 'thinking';
@@ -38,6 +42,7 @@ export interface ChatMessageRecord {
isPinned?: boolean;
pending?: boolean;
attachments?: ChatMessageAttachment[];
promptInvocation?: ProjectPromptInvocation;
}
export interface SessionBranchOrigin {
@@ -166,6 +171,20 @@ export function resolveSessionModelConfig(
};
}
export function normalizeChatMessageRecord(message: ChatMessageRecord): ChatMessageRecord {
const normalizedMessage: ChatMessageRecord = {
...message,
};
const promptInvocation = normalizeProjectPromptInvocation(message.promptInvocation);
if (promptInvocation) {
normalizedMessage.promptInvocation = promptInvocation;
} else {
delete normalizedMessage.promptInvocation;
}
return normalizedMessage;
}
export function applySessionModelConfig(
pattern: PatternDefinition,
session: SessionRecord,
+20
View File
@@ -1,6 +1,10 @@
import type { PatternDefinition } from '@shared/domain/pattern';
import { isScratchpadProject, type ProjectRecord } from '@shared/domain/project';
import type { ChatMessageAttachment } from '@shared/domain/attachment';
import {
normalizeProjectPromptInvocation,
type ProjectPromptInvocation,
} from '@shared/domain/projectCustomization';
import {
resolveSessionTitle,
type ChatMessageRecord,
@@ -175,11 +179,26 @@ function cloneAttachments(attachments?: ChatMessageAttachment[]): ChatMessageAtt
return cloned && cloned.length > 0 ? cloned : undefined;
}
function clonePromptInvocation(
promptInvocation?: ProjectPromptInvocation,
): ProjectPromptInvocation | undefined {
const cloned = normalizeProjectPromptInvocation(promptInvocation);
if (!cloned) {
return undefined;
}
return {
...cloned,
tools: cloned.tools ? [...cloned.tools] : undefined,
};
}
function cloneChatMessageRecord(message: ChatMessageRecord): ChatMessageRecord {
return {
...message,
pending: false,
attachments: cloneAttachments(message.attachments),
promptInvocation: clonePromptInvocation(message.promptInvocation),
};
}
@@ -386,6 +405,7 @@ export function editAndResendSessionRecord(
editedMessage.content = content;
editedMessage.attachments = cloneAttachments(attachments);
editedMessage.promptInvocation = undefined;
return {
...createDerivedSessionRecord(session, sessionId, editedAt),